Skip to main content

Order

An order is a package, or set of packages, you are sending to a recipient through a courier. Every other resource in the API exists to describe or act on one.

An order has a life cycle rather than a status field. A newly created order is a draft: you own it and can edit or delete it. Submitting it hands it to a courier, and the courier eventually places it on a route, at which point the order becomes read-only and progress starts reporting what is happening on the ground.

{
"id": "orders/W3kJpzQm9q4LV2BdH7sNuT",
"createdAt": 1778508502,
"recipient": {
"name": "Oliver Smith",
"email": "oliver.smith@example.com",
"phone": "+447700900123",
"externalId": null
},
"packageInfo": {
"products": ["Wireless headphones"],
"packageCount": 1,
"packageLoadAmount": 1.2,
"externalId": "PO-2026-04-21-1138"
},
"address": {
"address": "12 High Street, London SW1A 1AA, United Kingdom",
"addressLineOne": "12 High Street",
"addressLineTwo": "London SW1A 1AA, United Kingdom",
"latitude": 51.5074,
"longitude": -0.1278,
"placeId": "ChIJdd4hrwug2EcRmSrV3Vo6llI",
"placeTypes": ["street_address"]
},
"deliveryInstructions": {
"notes": "Leave with the neighbour",
"expectedDeliveryDate": { "year": 2026, "month": 5, "day": 15 },
"proofOfAttemptRequirements": { "enabled": true },
"handOffInfo": {
"handOffType": "pickup_by_courier",
"pickupLocationId": "pickupLocations/8Qm3RtBnZ9wXcLmPd2Fh"
}
},
"courier": null,
"submitted": false,
"submittedAt": null,
"progress": null,
"readOnly": false,
"source": "retailer",
"barcodes": ["4Z7X9K2L1M0N8B-1"]
}

Top-level fields

FieldMeaning
idThe order id, always orders/{id}. Append it to the base address to act on the order.
createdAtWhen Spoke created the order, in seconds since epoch. Set by Spoke.
recipientWho the delivery is for. See Recipient.
packageInfoWhat is being sent, and your own reference for it. See Package info.
addressWhere the delivery goes, after geocoding. See Address.
deliveryInstructionsHow and when to deliver. See Delivery instructions.
courierThe courier holding the order, as a couriers/{id}. null until you submit it.
submittedWhether the order has been handed to a courier.
submittedAtWhen it was submitted, in seconds since epoch. null while it is a draft.
progressWhat is happening to the delivery. null until the courier routes the order. See Progress.
readOnlyWhether you can still change the order. See readOnly.
sourceretailer if you created it, courier if the courier added it on your behalf. See source.
barcodesThe barcodes Spoke generated for this order, one per package, printed on its shipping labels. Changing packageInfo.packageCount regenerates the set.

readOnly

An order is read-only when you can no longer change it through the API. It becomes read-only for either of two reasons:

  • The courier put it on a route. From that moment the delivery is in motion and edits would not reach the driver.
  • The courier added it on your behalf, in which case it was never yours to edit.

Editing or deleting a read-only order answers 409 Conflict with code: "order_read_only". Withdrawing one that is already routed answers order_already_scheduled inside a 200 OK batch response.

source

source tells you where the order came from, and it never changes:

  • retailer: you created it, through this API or the Spoke Connect app.
  • courier: the courier created it on your behalf. Courier-originated orders are surfaced read-only rather than hidden, so what you see through the API is everything addressed to your account, not just what you put there.

Recipient

Every field is optional and defaults to null. Spoke does not require recipient details to create an order, but a courier may refuse to accept one without them.

FieldMeaning
nameThe recipient's full name.
emailThe recipient's email address. Used for delivery notifications where the courier sends them.
phoneThe recipient's phone number.
externalIdYour own id for this recipient in your system. Spoke stores it and hands it back, nothing more.

Package info

FieldMeaning
productsWhat is in the package, as free text. Shown to the driver. Defaults to [].
packageCountHow many physical packages make up this order. Defaults to 1. One barcode and one label page is produced per package.
packageLoadAmountHow much of a vehicle's capacity the order takes up, in whatever unit your account is configured with. null when not recorded, which is not the same as zero.
externalIdYour own id for this order. This is the field GET /orders?filter.externalId= matches on. Spoke does not enforce uniqueness, so a reused value returns several orders.

Address

The address on an order is the resolved address, not what you sent. Spoke geocodes your input at creation time and stores the result, which is what makes the order routable.

FieldMeaning
addressThe full address as one string, computed by geocoding. Empty when the order was created from coordinates alone.
addressLineOneFirst line of the resolved address.
addressLineTwoSecond line of the resolved address.
latitudeResolved latitude in decimal degrees, or null when geocoding produced no coordinates.
longitudeResolved longitude in decimal degrees, or null when geocoding produced no coordinates.
placeIdThe Google Places identifier for the location, when one was matched.
placeTypesGoogle Places types for the match, for example ["street_address"]. Useful for telling a precise match from a broad one.

What you send is a different shape. On create and update you may send addressLineOne, addressLineTwo, city, state, zip, country, addressName, latitude, and longitude. At least one of them has to be able to locate the delivery, and addressName is a display-only label that does not count. Coordinates must be sent as a pair and override the line fields for geocoding. The four computed fields above are never accepted as input.

On an update, an address you send replaces the stored one and is geocoded again from scratch. It is not merged field by field.

Delivery instructions

FieldMeaning
notesFree-text instructions shown to the driver, for example where to leave the package.
expectedDeliveryDateThe calendar day you expect delivery on, as { year, month, day }. Date-only and in your local timezone, because it names a day rather than an instant.
proofOfAttemptRequirementsWhether the driver has to capture proof. See below.
handOffInfoHow the package gets to the courier. See below.

proofOfAttemptRequirements

A single field, enabled, with three meaningful values:

  • true: the driver must capture proof, a signature or a photo, following the courier's own per-attempt-type policy.
  • false: no proof is required for this order.
  • null: use the courier's default.

Proof is only enforced where the courier's subscription supports it, so true is a request rather than a guarantee.

handOffInfo

How the package reaches the courier.

FieldMeaning
handOffTypeship_to_courier when you send the package to the courier, pickup_by_courier when the courier collects from you. null means your account default applies.
pickupLocationIdThe pickup location the courier collects from. Always null for ship_to_courier. null with pickup_by_courier means your main location.

On an update, handOffType cannot be set back to null, and switching to ship_to_courier clears the stored pickupLocationId.

Progress

progress is null until the courier puts the order on a route. Once it is populated it is the only place to look for what happened to the delivery.

FieldMeaning
trackingLinkThe tracking page the recipient can follow. Safe to pass on, unlike a shipping label link.
etaThe arrival estimate, as a tagged union. See eta.
attemptedWhether the driver has been to the address, successfully or not.
attemptedAtWhen the attempt happened, in seconds since epoch. null before the attempt.
stateWhat happened on the attempt. See state.
routeStartDateThe day the route runs, as { year, month, day }.
routeStartedWhether the driver has started the route. Before this, an ETA is a plan rather than a live estimate.
succeededWhether the delivery worked. Read together with state, which says why.
photoUrlsProof-of-attempt photos the driver captured. A URL can answer not-found briefly while the driver's app finishes uploading, so retry rather than treating it as missing.
signatureUrlThe captured signature image, when one was collected. Same upload delay as photos.
signeeNameWho signed, when a signature was collected.
driverProvidedRecipientNotesA note the driver left for the recipient.

eta

A tagged union, so read status before reaching for value:

  • { "status": "available", "value": { ... } }: value carries estimatedArrivalAt plus the estimatedEarliestArrivalAt and estimatedLatestArrivalAt bounds of the arrival window, all in seconds since epoch.
  • { "status": "pending" }: the order is on a route, but there is no estimate yet because the route is not optimized. Check again later.
  • { "status": "restricted" }: the courier's subscription does not include ETAs. This will not change for this order, so do not poll waiting for it.

ETAs are recomputed on every read from the route's current state rather than stored on the order, so they move as the driver does.

state

The outcome of the delivery attempt. unattempted until the driver has been.

GroupValues
Delivereddelivered_to_recipient, delivered_to_third_party, delivered_to_mailbox, delivered_to_safe_place, delivered_to_pickup_point, delivered_other
Picked uppicked_up_from_customer, picked_up_unmanned, picked_up_from_locker, picked_up_other
Failedfailed_not_home, failed_cant_find_address, failed_no_parking, failed_no_time, failed_package_not_available, failed_missing_required_proof, failed_payment_not_received, failed_other
Pendingunattempted

New values can be added without a new API version, so treat this as an open set and fall back to succeeded when you meet one you do not recognise.