Submit an order, and withdraw it
Submitting hands a draft order to a courier. Withdrawing pulls it back, but only while the courier has not yet put it on a route. This page walks both calls as one story, because the interesting part is where the window closes.
Both endpoints are batch endpoints even for a single order, and both take at most 100 ids per request.
Find the courier
GET /couriers lists the couriers
your account is connected to. Connections are made in the Spoke Connect app, so
this endpoint is read-only.
curl "https://api.spoke.com/connect/v1/couriers" \
-H "Authorization: Bearer <api-key>"
{
"couriers": [
{ "id": "couriers/QyZ8mp34RFlIfwREp1js", "name": "Northside Logistics" }
],
"nextPageToken": null
}
Submit
POST /orders:submit takes the order
ids and one courier id, both with their collection prefix:
curl "https://api.spoke.com/connect/v1/orders:submit" \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"orderIds": ["orders/W3kJpzQm9q4LV2BdH7sNuT"],
"courierId": "couriers/QyZ8mp34RFlIfwREp1js"
}'
{
"success": ["orders/W3kJpzQm9q4LV2BdH7sNuT"],
"failed": []
}
Like the import endpoint, this answers 200 OK whenever the request itself is
valid, including when every order failed. Check failed, not just the status
code.
Read the order back and it now carries the courier (abridged to the fields that changed):
{
"id": "orders/W3kJpzQm9q4LV2BdH7sNuT",
"courier": "couriers/QyZ8mp34RFlIfwREp1js",
"submitted": true,
"submittedAt": 1778511103,
"progress": null,
"readOnly": false
}
progress is still null and readOnly is still false. That combination is
what tells you the courier has accepted the order but has not routed it yet,
which is exactly the window in which a withdraw succeeds.
Per-order failures
code | What happened |
|---|---|
order_not_found | No such order for your account, or it was deleted. |
order_already_submitted | The order is already with a courier. Withdraw it before resubmitting. |
order_invalid_fields | The order is missing a field your account requires, or its address is beyond a maximum distance from the pickup your account has set. |
order_concurrent_modification | The order changed mid-request. Read it back and retry. |
Request-level failures
A courier problem fails the whole request with 422, because a batch submitted
to the wrong courier has no partially correct outcome:
{
"message": "No connected courier matches `courierId`.",
"code": "order_invalid_fields",
"param": "courierId"
}
{
"message": "This courier charges for deliveries, so orders cannot be submitted to it through the API. Submit them from the Spoke Connect app, which handles the payment.",
"code": "order_courier_requires_payment",
"param": "courierId"
}
The second one is a real constraint rather than a misconfiguration. The API only serves the free path, so orders going to a courier that charges per delivery have to be submitted from the Spoke Connect app, which handles payment.
Withdraw
POST /orders:withdraw takes only
order ids. There is no courier to name, since the order already knows which
courier holds it.
curl "https://api.spoke.com/connect/v1/orders:withdraw" \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"orderIds": ["orders/W3kJpzQm9q4LV2BdH7sNuT"]}'
{
"success": ["orders/W3kJpzQm9q4LV2BdH7sNuT"],
"failed": []
}
The order is a draft again: courier is back to null, submitted is false,
and you can edit or delete it.
When the window closes
Once the courier puts the order on a route, it is out of your hands. The same
withdraw now fails per-order, still inside a 200 OK:
{
"success": [],
"failed": [
{
"orderId": "orders/W3kJpzQm9q4LV2BdH7sNuT",
"error": {
"code": "order_already_scheduled",
"message": "The order is already on a route and can no longer be withdrawn."
}
}
]
}
Withdraw takes no courier, so there is no request-level rejection beyond schema validation. Every ineligible order is a per-order outcome:
code | What happened |
|---|---|
order_already_scheduled | The courier already routed it. This is the closed window. |
order_not_submitted | The order was never submitted, so there is nothing to withdraw. |
order_read_only | The order can no longer be changed through the API. |
order_courier_managed | The courier added this order on your behalf, so the courier owns it. |
order_no_courier_link | The order is not linked to a courier. |
order_not_found | No such order for your account, or it was deleted. |
order_concurrent_modification | The order changed mid-request. Read it back and retry. |
If you need to cancel an order after it has been routed, contact the courier. The API cannot take it back.