External Shipper Scheduling - Grasshopper Labs API
API Docs v1
Shipper Scheduling

Shipper Scheduling

Let your Shippers fetch available delivery dates and book their own orders onto a manifest, straight from their OMS, storefront, or customer service tooling. Two endpoints, one clean flow.

These endpoints are built for the Shipper side of the network. A Shipper's system checks which delivery dates are open for a destination zip, presents them to the end customer, and books the order onto a manifest for the chosen date. Grasshopper handles manifest selection, capacity, and dedicated-shipper rules behind the scenes; internal manifest IDs, routes, and capacity details are never exposed.

Endpoints

Authentication and Shipper scope

All endpoints require an authenticated Shipper user. Send a valid Grasshopper REST auth token in the Authorization header. See Authentication for how to obtain and refresh tokens.

  • :Shipper_identifier is the Shipper's Grasshopper identifier, for example Shipperidentifiercode. Mongo Shipper IDs are not accepted in this path.
  • The authenticated user must be associated with that Shipper otherwise the API returns 403.
  • All request and response dates use YYYY-MM-DD. Plain date strings, no time or timezone component.
Base URL. Replace {licensee-url} with the production URL of the licensee you are integrating with. Your licensee provides this together with your credentials and Shipper identifier.

Get available dates

Returns the distinct delivery dates that are open for a destination zip code. Dedicated-shipper rules are applied for your Shipper before dates are deduplicated, so the list you get is exactly what your customer can book.

GET/api/external/Shippers/:Shipper_identifier/available_dates

Query parameters

zip_codestringRequired

Destination zip code for the delivery.

from_datestring (YYYY-MM-DD)Optional

Earliest date to return. Defaults to tomorrow.

to_datestring (YYYY-MM-DD)Optional

Latest date to return. Defaults to from_date + 14 days.

The requested range must be at most 30 days (to_date minus from_date). Longer ranges return 400.

curl "https://{licensee-url}/api/external/Shippers/Shipperidentifiercode/available_dates?zip_code=30301&from_date=2026-08-10&to_date=2026-08-31" \
  -H "Authorization: Bearer $TOKEN"
{
  "status": "ok",
  "data": {
    "zip_code": "30301",
    "dates": [
      "2026-08-14",
      "2026-08-15"
    ]
  }
}
# A covered zip with no available manifests still returns 200
{
  "status": "ok",
  "data": {
    "zip_code": "30301",
    "dates": []
  }
}
What you get back. Distinct available dates only. Internal manifest IDs, routes, and capacity details are not exposed. An empty dates array means the zip is covered but nothing is open in the requested window; widen the range (up to 30 days) or try again later.

Try it out

Response will appear here

Schedule an order

Books one of your orders onto a manifest for the chosen date. The order lookup is always scoped to the Shipper resolved from :Shipper_identifier, and the API applies the same dedicated-shipper and capacity filtering as public tracking. If multiple manifests are available on the selected date, the first eligible manifest is booked, matching existing public scheduling behavior.

POST/api/external/Shippers/:Shipper_identifier/schedule

Body parameters

datestring (YYYY-MM-DD)Required

The delivery date to book. Must be a date returned by the available dates endpoint.

order_idstringOne of two

Grasshopper order ID. If supplied, it is always used and po_number is ignored.

po_numberstringOne of two

Your Shipper PO number. Used only when order_id is omitted.

If a PO resolves to multiple orders, the API returns 409. Resend with order_id to disambiguate.

Send exactly one identifier. Prefer order_id whenever you have it. It always wins, it can never be ambiguous, and it avoids the 409 path entirely. Supplying neither identifier returns 400.
curl -X POST https://{licensee-url}/api/external/Shippers/Shipperidentifiercode/schedule \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": "ORDER-123",
    "date": "2026-08-14"
  }'
curl -X POST https://{licensee-url}/api/external/Shippers/Shipperidentifiercode/schedule \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "po_number": "PO-98765",
    "date": "2026-08-14"
  }'
{
  "status": "ok",
  "data": {
    "order_id": "ORDER-123",
    "po_number": "PO-98765",
    "scheduled_date": "2026-08-14",
    "scheduled": true
  }
}
Validations still apply. All normal manifest validations run on every schedule call, including payment, hold status, existing schedules, capacity, account status, and tag-along rules. When the order cannot be added to the manifest, the existing scheduling flow returns its own validation status codes.

Try it out

Response will appear here

Errors

Errors use the standard API envelope:

{
  "status": 404,
  "message": "Order not found for this Shipper"
}
StatusCauseHow to handle
400Invalid or missing parameters, including when neither order identifier is supplied, or a date range longer than 30 days.Fix the request. Validate inputs client side before calling.
401Missing or invalid access token.Refresh the token and retry. See Refresh token.
403Authenticated user does not belong to the requested Shipper.Confirm the user is associated with the Shipper via companies[].company_id or company_id, and that :Shipper_identifier is the Grasshopper identifier, not a Mongo ID.
404Shipper, zip, order, or an eligible manifest for the date was not found.On schedule calls this often means the date filled up after you fetched it. Refetch available dates and reprompt.
409The PO number resolves to multiple orders for the Shipper.Resend the request with order_id to disambiguate.
4xxOther validation status codes from the existing scheduling flow when the order cannot be added to the manifest (payment, hold status, existing schedule, capacity, account status, tag-along rules).Surface the message to your ops team or customer service. These are order state issues, not integration bugs.

Integration tutorial

End to end, the flow is: authenticate, fetch open dates for the destination zip, let the customer pick one, and book it. Most teams ship this in under a day.

1

Get your credentials and Shipper identifier

Your licensee provides three things: the production base URL ({licensee-url}), Shipper user credentials, and your :Shipper_identifier. Then authenticate per the Authentication guide to obtain a bearer token.

2

Fetch available dates for the destination zip

Call the available dates endpoint with the consignee's zip. Do this right before rendering your date picker; availability changes as manifests fill, so avoid caching results for more than a few minutes.

3

Book the chosen date

POST to the schedule endpoint with the date and one identifier, preferably order_id. Handle 409 by retrying with order_id, and handle 404 by refetching dates, since the slot may have filled between fetch and booking.

4

Confirm and track

On success, data.scheduled is true and data.scheduled_date is the booked date. Show that to the customer. To react to downstream changes such as reschedules or delivery events, subscribe to Webhooks.

Full working example

const BASE_URL = "https://{licensee-url}";
const Shipper = "Shipperidentifiercode";
const TOKEN = process.env.GRASSHOPPER_TOKEN;

async function getAvailableDates(zipCode, fromDate, toDate) {
  const params = new URLSearchParams({ zip_code: zipCode });
  if (fromDate) params.set("from_date", fromDate);
  if (toDate) params.set("to_date", toDate);

  const res = await fetch(
    `${BASE_URL}/api/external/Shippers/${Shipper}/available_dates?${params}`,
    { headers: { Authorization: `Bearer ${TOKEN}` } }
  );
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.status}: ${body.message}`);
  return body.data.dates; // ["2026-08-14", "2026-08-15"]
}

async function scheduleOrder({ orderId, poNumber, date }) {
  // Send exactly one identifier. order_id always wins.
  const payload = orderId
    ? { order_id: orderId, date }
    : { po_number: poNumber, date };

  const res = await fetch(
    `${BASE_URL}/api/external/Shippers/${Shipper}/schedule`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    }
  );
  const body = await res.json();

  if (res.status === 409) {
    // PO matched more than one order. Retry with order_id.
    throw new Error("Ambiguous PO number. Retry with order_id.");
  }
  if (!res.ok) throw new Error(`${body.status}: ${body.message}`);
  return body.data; // { order_id, po_number, scheduled_date, scheduled: true }
}

// End to end: fetch dates, then book the first one
const dates = await getAvailableDates("30301");
if (dates.length === 0) {
  // Covered zip, nothing open in the window. Widen the range or retry later.
}
const result = await scheduleOrder({ orderId: "ORDER-123", date: dates[0] });
console.log(result.scheduled_date); // "2026-08-14"
import os
import requests

BASE_URL = "https://{licensee-url}"
Shipper = "Shipperidentifiercode"
TOKEN = os.environ["GRASSHOPPER_TOKEN"]

session = requests.Session()
session.headers.update({"Authorization": f"Bearer {TOKEN}"})


def get_available_dates(zip_code, from_date=None, to_date=None):
    params = {"zip_code": zip_code}
    if from_date:
        params["from_date"] = from_date
    if to_date:
        params["to_date"] = to_date

    res = session.get(
        f"{BASE_URL}/api/external/Shippers/{Shipper}/available_dates",
        params=params,
    )
    body = res.json()
    if not res.ok:
        raise RuntimeError(f"{body['status']}: {body['message']}")
    return body["data"]["dates"]


def schedule_order(date, order_id=None, po_number=None):
    # Send exactly one identifier. order_id always wins.
    if order_id:
        payload = {"order_id": order_id, "date": date}
    elif po_number:
        payload = {"po_number": po_number, "date": date}
    else:
        raise ValueError("Provide order_id or po_number")

    res = session.post(
        f"{BASE_URL}/api/external/Shippers/{Shipper}/schedule",
        json=payload,
    )
    body = res.json()

    if res.status_code == 409:
        # PO matched more than one order. Retry with order_id.
        raise RuntimeError("Ambiguous PO number. Retry with order_id.")
    if not res.ok:
        raise RuntimeError(f"{body['status']}: {body['message']}")
    return body["data"]


# End to end: fetch dates, then book the first one
dates = get_available_dates("30301")
if dates:
    result = schedule_order(dates[0], order_id="ORDER-123")
    print(result["scheduled_date"])  # "2026-08-14"

Best practices

  • Fetch dates just in time. Availability shifts as manifests fill. Pull dates right before showing the picker and treat anything older than a few minutes as stale.
  • Empty is not an error. A covered zip with no open manifests returns 200 with an empty dates array. Widen the window up to the 30 day maximum, or show a "check back soon" state.
  • Prefer order_id. It always takes precedence, cannot be ambiguous, and skips the 409 path. Use po_number only when your system does not hold the Grasshopper order ID.
  • Handle the race on booking. A 404 on schedule usually means the date filled between fetch and book. Refetch dates and reprompt instead of surfacing a hard failure.
  • Do not resubmit a successful booking. Orders with an existing schedule fail normal manifest validation. Confirm off the scheduled: true response and stop.
  • Dates are plain strings. YYYY-MM-DD with no timezone. Display them as received; do not run them through local timezone conversion.

Questions?

Reach the API team at support@grasshopperlabs.io or submit a ticket. For token issues, start with Authentication.