Skip to main content

Create an order

POST /orders creates one draft order. Only address is required, and it only has to carry a single field that can locate the delivery. Everything else is optional and falls back to your account defaults.

A new order is a draft: no courier owns it yet, so you can still edit it, delete it, or add it to a batch you submit later.

Create the order​

curl "https://api.spoke.com/connect/v1/orders" \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"recipient": {
"name": "Oliver Smith",
"email": "oliver.smith@example.com",
"phone": "+447700900123"
},
"packageInfo": {
"products": ["Wireless headphones"],
"packageCount": 1,
"weight": { "amount": 1.2, "unit": "kilogram" },
"externalId": "PO-2026-04-21-1138"
},
"address": {
"addressLineOne": "12 High Street",
"addressLineTwo": "London",
"zip": "SW1A 1AA",
"country": "United Kingdom"
},
"deliveryInstructions": {
"notes": "Leave with the neighbour",
"expectedDeliveryDate": { "year": 2026, "month": 5, "day": 15 },
"proofOfAttemptRequirements": { "enabled": true },
"handOffInfo": { "handOffType": "ship_to_courier" }
}
}'

The response is 201 Created with the full order:

{
"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": 1200,
"weight": { "amount": 1.2, "unit": "kilogram" },
"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": "ship_to_courier",
"pickupLocationId": null
}
},
"courier": null,
"submitted": false,
"submittedAt": null,
"progress": null,
"readOnly": false,
"source": "retailer",
"barcodes": ["4Z7X9K2L1M0N8B-1"]
}

Four things in that response are worth pointing out:

  • address came back rewritten. Spoke geocodes the address you send and stores the resolved one, along with latitude, longitude, placeId, and placeTypes. Those four are response-only and are not accepted on the way in.
  • barcodes is already populated, one barcode per package in packageInfo.packageCount. Those are the barcodes that end up on the shipping labels.
  • courier and progress are null and submitted is false, because nothing has been submitted to a courier yet.
  • source is retailer, meaning you created it. Orders a courier adds on your behalf come back as source: "courier" and are always read-only.

Have the courier collect instead​

The order above ships to the courier. To have the courier collect from one of your own locations, set the hand-off type and name the pickup location. List them with GET /pickupLocations first:

curl "https://api.spoke.com/connect/v1/pickupLocations" \
-H "Authorization: Bearer <api-key>"
{
"pickupLocations": [
{ "id": "pickupLocations/8Qm3RtBnZ9wXcLmPd2Fh", "name": "Camden warehouse" }
],
"nextPageToken": null
}

Then pass the id straight through, prefix included:

{
"address": { "addressLineOne": "12 High Street", "addressLineTwo": "London" },
"deliveryInstructions": {
"handOffInfo": {
"handOffType": "pickup_by_courier",
"pickupLocationId": "pickupLocations/8Qm3RtBnZ9wXcLmPd2Fh"
}
}
}

Omit pickupLocationId and the courier collects from your main location.

When it fails​

StatuscodeWhat happened
400validationThe body does not match the schema, for example an address with no usable field.
401The API key is missing, invalid, or expired.
422order_address_unresolvableSpoke could not geocode the address. Nothing was created, so fix the address and send it again.
422order_invalid_fieldsA field was rejected. param names it, most often pickupLocationId.

An unresolvable address looks like this:

{
"message": "The order address could not be resolved.",
"code": "order_address_unresolvable"
}

Geocoding runs before anything is written, so a 422 here always means no order was created. Retrying with a corrected address is safe.

Edit it before it ships​

PATCH /orders/{id} changes only what you send. The order's id goes straight onto the base address:

curl -X PATCH "https://api.spoke.com/connect/v1/orders/W3kJpzQm9q4LV2BdH7sNuT" \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"deliveryInstructions": {"notes": "Ring the bell twice"}}'

The response is 200 OK with the updated order. Read On Update endpoints before you rely on the edit semantics, especially for address, which is replaced rather than merged.