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
Distinct open delivery dates for a destination zip within a date range.
POST/api/external/Shippers/:Shipper_identifier/schedule Schedule an orderBook an order onto a manifest for a chosen date, by order ID or PO number.
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_identifieris the Shipper's Grasshopper identifier, for exampleShipperidentifiercode. 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.
{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.
Query parameters
zip_codestringRequiredDestination zip code for the delivery.
from_datestring (YYYY-MM-DD)OptionalEarliest date to return. Defaults to tomorrow.
to_datestring (YYYY-MM-DD)OptionalLatest 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": [] } }
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.
Body parameters
datestring (YYYY-MM-DD)RequiredThe delivery date to book. Must be a date returned by the available dates endpoint.
order_idstringOne of twoGrasshopper order ID. If supplied, it is always used and po_number is ignored.
po_numberstringOne of twoYour 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.
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
}
}Try it out
Response will appear here
Errors
Errors use the standard API envelope:
{
"status": 404,
"message": "Order not found for this Shipper"
}| Status | Cause | How to handle |
|---|---|---|
| 400 | Invalid 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. |
| 401 | Missing or invalid access token. | Refresh the token and retry. See Refresh token. |
| 403 | Authenticated 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. |
| 404 | Shipper, 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. |
| 409 | The PO number resolves to multiple orders for the Shipper. | Resend the request with order_id to disambiguate. |
| 4xx | Other 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.
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.
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.
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.
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
200with an emptydatesarray. 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 the409path. Usepo_numberonly when your system does not hold the Grasshopper order ID. - Handle the race on booking. A
404on 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: trueresponse and stop. - Dates are plain strings.
YYYY-MM-DDwith 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.