View the Markdown
```meta
title: Orders REST API
subtitle: What a client can call on the orders service, what each call returns, and what to do when it fails.
tag: API v1
```
The orders service exposes one public REST API at `https://api.example.com/v1`.
Every request carries `Authorization: Bearer <api_key>` and every body is JSON.
The version lives in the path; a breaking change ships as `/v2` and `/v1` stays
up for twelve months after.
Every `POST` takes an `Idempotency-Key` header. The service stores the first
response for a key for 24 hours. A retry with the same key and the same body
gets that stored response. A retry with the same key and a different body gets `409`.
List calls page with an opaque `cursor`; a page never repeats or skips an order
while you walk it.
An order's `status` is one of the states in [order-lifecycle](order-lifecycle.md).
The API never lets a client set `status` directly. `POST /orders` creates the
order in `PAYMENT_PENDING` and `POST /orders/{id}/cancel` moves it to `CANCELLED`.
Every other transition belongs to payment, warehouse, or carrier events.
```callout
tone: note
title: Assumptions
body: "Auth is a per-merchant API key, not OAuth. Amounts are integer minor units (cents) with an ISO 4217 currency. Payment is captured through the hosted gateway at checkout, so an order reaches PAID without a second API call. A client can cancel only while the order is BACKORDERED or RESERVED; after PACKED the parcel belongs to the carrier. The webhook feed is documented separately."
```
## Create an order
```endpoint
method: POST
path: /v1/orders
description: "Submit line items and a shipping address. The order starts in PAYMENT_PENDING and the gateway captures the payment asynchronously."
auth: Bearer api_key
params:
- { name: Idempotency-Key, in: header, type: string, required: true, desc: "Unique per attempt. Reuse it on retry to get the original response." }
body:
- { name: items, type: "LineItem[]", required: true, desc: "One or more of { sku, qty }. qty is 1..99." }
- { name: shipping_address, type: Address, required: true, desc: "name, line1, line2, city, postal_code, country (ISO 3166-1 alpha-2)." }
- { name: currency, type: string, required: true, desc: "ISO 4217 code, for example USD." }
- { name: payment_method_id, type: string, required: true, desc: "Token from the hosted payment gateway, prefix pm_." }
- { name: customer_reference, type: string, desc: "Free text the client can search by later, max 64 chars." }
responses:
- { status: 201, desc: "Order created; status is PAYMENT_PENDING." }
- { status: 400, desc: "Malformed JSON or a field fails validation." }
- { status: 401, desc: "Missing or invalid API key." }
- { status: 409, desc: "Idempotency-Key reused with a different body." }
- { status: 422, desc: "A sku is unknown or has no stock (code sku_unknown or out_of_stock)." }
request: |
{
"items": [{ "sku": "MUG-BLUE-12OZ", "qty": 2 }],
"shipping_address": {
"name": "Ada Lovelace", "line1": "12 Analytical St",
"city": "London", "postal_code": "N1 9GU", "country": "GB"
},
"currency": "GBP",
"payment_method_id": "pm_9f3k2",
"customer_reference": "web-checkout-77120"
}
response: |
{
"id": "ord_01J8Q2ZK4M",
"status": "PAYMENT_PENDING",
"currency": "GBP",
"total_cents": 2400,
"items": [{ "sku": "MUG-BLUE-12OZ", "qty": 2, "unit_price_cents": 1200 }],
"created_at": "2026-09-13T10:02:11Z",
"updated_at": "2026-09-13T10:02:11Z"
}
```
## Fetch one order
```endpoint
method: GET
path: "/v1/orders/{order_id}"
description: "Return the current order, including its status and the shipment tracking number once it is SHIPPED."
auth: Bearer api_key
params:
- { name: order_id, in: path, type: string, required: true, desc: "Order id, prefix ord_." }
responses:
- { status: 200, desc: "The order." }
- { status: 401, desc: "Missing or invalid API key." }
- { status: 404, desc: "No order with this id belongs to the calling merchant." }
response: |
{
"id": "ord_01J8Q2ZK4M",
"status": "SHIPPED",
"currency": "GBP",
"total_cents": 2400,
"items": [{ "sku": "MUG-BLUE-12OZ", "qty": 2, "unit_price_cents": 1200 }],
"shipment": { "carrier": "Royal Mail", "tracking_number": "RM123456789GB" },
"created_at": "2026-09-13T10:02:11Z",
"updated_at": "2026-09-14T08:41:00Z"
}
```
## List orders
```endpoint
method: GET
path: /v1/orders
description: "Page through the calling merchant's orders, newest first. Filter by status or by the customer_reference given at creation."
auth: Bearer api_key
params:
- { name: status, in: query, type: string, desc: "One lifecycle status, for example RESERVED. Omit for all." }
- { name: customer_reference, in: query, type: string, desc: "Exact match on the value given at creation." }
- { name: limit, in: query, type: integer, desc: "Page size 1..100, default 20." }
- { name: cursor, in: query, type: string, desc: "Opaque token from the previous page's next_cursor." }
responses:
- { status: 200, desc: "A page of orders and next_cursor, null on the last page." }
- { status: 400, desc: "Unknown status value, limit out of range, or a malformed cursor." }
- { status: 401, desc: "Missing or invalid API key." }
response: |
{
"data": [
{ "id": "ord_01J8Q2ZK4M", "status": "SHIPPED", "total_cents": 2400, "currency": "GBP" },
{ "id": "ord_01J8PXV0TC", "status": "RESERVED", "total_cents": 5900, "currency": "GBP" }
],
"next_cursor": "eyJpZCI6Im9yZF8wMUo4UFhWMFRDIn0"
}
```
## Cancel an order
```endpoint
method: POST
path: "/v1/orders/{order_id}/cancel"
description: "Move a BACKORDERED or RESERVED order to CANCELLED. The refund runs on its own and the order reaches REFUNDED when it settles."
auth: Bearer api_key
params:
- { name: order_id, in: path, type: string, required: true, desc: "Order id, prefix ord_." }
- { name: Idempotency-Key, in: header, type: string, required: true, desc: "Unique per attempt." }
body:
- { name: reason, type: string, required: true, desc: "One of customer_request, fraud, duplicate." }
- { name: note, type: string, desc: "Free text shown to support, max 500 chars." }
responses:
- { status: 200, desc: "Order is now CANCELLED." }
- { status: 401, desc: "Missing or invalid API key." }
- { status: 404, desc: "No order with this id belongs to the calling merchant." }
- { status: 409, desc: "Status is not BACKORDERED or RESERVED (code invalid_transition); the response carries the current status." }
request: |
{ "reason": "customer_request", "note": "Ordered the wrong colour" }
response: |
{
"id": "ord_01J8PXV0TC",
"status": "CANCELLED",
"cancelled_at": "2026-09-13T11:15:42Z",
"refund": { "amount_cents": 5900, "status": "pending" }
}
```
## Change the shipping address
```endpoint
method: PATCH
path: "/v1/orders/{order_id}/shipping-address"
description: "Replace the shipping address at any status before PACKED. Once PACKED the label is printed and the address is fixed."
auth: Bearer api_key
params:
- { name: order_id, in: path, type: string, required: true, desc: "Order id, prefix ord_." }
body:
- { name: shipping_address, type: Address, required: true, desc: "Full replacement; partial updates are not merged." }
responses:
- { status: 200, desc: "The order with the new address." }
- { status: 401, desc: "Missing or invalid API key." }
- { status: 404, desc: "No order with this id belongs to the calling merchant." }
- { status: 409, desc: "Order is PACKED or later (code invalid_transition)." }
- { status: 422, desc: "Address fails validation, for example country not served (code address_invalid)." }
request: |
{
"shipping_address": {
"name": "Ada Lovelace", "line1": "4 Ockham Rd",
"city": "Guildford", "postal_code": "GU1 3AB", "country": "GB"
}
}
```
## Create, confirm, cancel
The only ordering rule a client must respect is the one below: a retry of
`POST /orders` must reuse the `Idempotency-Key`, or the customer is charged
twice. A cancel is safe to send at any time; the `409` tells the client the
order has already moved past the point of no return.
```sequence
id: create-confirm-cancel
actors:
- { id: Client, name: Client }
- { id: API, name: Orders API }
- { id: Gateway, name: Payment gateway, external: true }
messages:
- Client -> +API: "POST /v1/orders (Idempotency-Key k1)"
- API -> Gateway: "authorize pm_9f3k2, 2400 GBP"
- API --> -Client: "201 PAYMENT_PENDING"
- opt: client timed out and retries
- Client -> +API: "POST /v1/orders (Idempotency-Key k1)"
- API --> -Client: "201 replayed, same order id"
- end
- Gateway -> API: "capture confirmed"
- loop: until status leaves PAYMENT_PENDING
- Client -> +API: "GET /v1/orders/ord_01J8Q2ZK4M"
- API --> -Client: "200 PAID, then RESERVED"
- end
- Client -> +API: "POST /v1/orders/ord_01J8Q2ZK4M/cancel"
- alt: status is BACKORDERED or RESERVED
- API -> Gateway: "refund 2400 GBP"
- API --> -Client: "200 CANCELLED"
- else: status is PACKED or later
- API -x-> Client: "409 invalid_transition"
- end
foot:
- { label: Polling interval, value: "2 s, back off to 30 s" }
- { label: Capture latency, value: "p95 under 4 s" }
```
## Error codes
Every non-2xx response has the same envelope:
`{ "error": { "code": "...", "message": "...", "details": [] } }`. The `code`
is stable and is the value to branch on; `message` is for logs, not for logic.
```table
id: error-codes
columns: [HTTP, code, Meaning, Client action]
rows:
- [400, invalid_request, "JSON does not parse or a field fails validation; details names the field.", "Fix the request. Do not retry as-is."]
- [401, unauthorized, "API key missing, revoked, or for another environment.", "Check the key and the base URL. Do not retry."]
- [403, forbidden, "Key is valid but the merchant has no orders scope.", "Ask the account owner for the scope."]
- [404, not_found, "No such order for this merchant.", "Treat as gone; check the id."]
- [409, idempotency_conflict, "Idempotency-Key reused with a different body.", "Generate a new key for a new request."]
- [409, invalid_transition, "The order's status does not allow this call; details carries the current status.", "Read the status and stop; the order has moved on."]
- [422, sku_unknown, "A sku in items does not exist.", "Remove or correct the item."]
- [422, out_of_stock, "A sku has no stock now; details lists the skus.", "Offer the customer a substitute or a wait."]
- [422, address_invalid, "Shipping address fails validation or country is not served.", "Correct the address."]
- [429, rate_limited, "More than 100 requests per second for this key.", "Wait for Retry-After seconds, then retry."]
- [500, internal, "Unexpected server error; the request was not applied.", "Retry with the same Idempotency-Key after a backoff."]
- [503, unavailable, "Payment gateway or warehouse unreachable.", "Retry with the same Idempotency-Key after Retry-After."]
note: "Every 5xx is safe to retry with the same Idempotency-Key. No 4xx is."
```
## Terms
```glossary
terms:
- "Order — one purchase with its items, address, payment, and a single status."
- "Line item — one sku and its qty inside an order; the price is fixed at creation."
- "Status — the order's current lifecycle state, set only by the service."
- "Idempotency-Key — client-chosen header that makes a POST safe to retry for 24 hours."
- "Cursor — opaque token that continues a list from where the last page ended."
- "Merchant — the account an API key belongs to; every order belongs to exactly one."
```