Create an order
POST /v1/orders — requires the orders:write scope.
Request body
About
items:
- Every product must belong to the key’s store, be
listed, and share a single currency with the rest of the cart. - A
productIdthat appears more than once has its quantities summed into one line — a cart is deduplicated, not rejected. - The order total is Σ (product price × quantity).
The Idempotency-Key header
Optional, 1 to 255 characters, unique per store.
Retrying a create with a key this store has already used returns the original
order — same id, same everything — instead of minting a duplicate. This holds no
matter how the request body changed: one key, one order.
Keys never expire, so a create months later that reuses an old key returns that old
order. Use one key per logical purchase; your own purchase id is the natural choice.
A key outside the 1–255 range is rejected with
{ "error": "Idempotency-Key must be between 1 and 255 characters" }.
Example
201 Created
unitPrice is a fiat decimal string with no trailing-zero padding — "50" is
$50.00. The create response deliberately returns only what you need to proceed;
GET /v1/orders/{id} returns the full order.Errors
All of these are400 with a { "error": … } body.
Anything the request schema rejects — an empty items, more than 20 line items,
expiresIn outside 10 to 129,600, a quantity outside 1 to 1,000,000, a productId
that is not a UUID, a successUrl that is not http(s) or is over 2048 characters —
never reaches the order logic. The validator answers first, always with the same
generic body:
400 Bad Request
Quantity bounds come from the product’s delivery configuration and default to a
minimum of 1 and no maximum. See
Products and delivery.
Product does not belong to this store distinguishes “another store’s id” from “no
such id”, where the rest of the API reports a resource you cannot see as simply
missing. Treat it as a signal you have crossed keys and stores — a live key against
a test store’s product ids, say — not as a lookup you should depend on.Retrieve an order
GET /v1/orders/{id} — requires orders:read. {id} is the order’s public short id.
200 OK
The best-funded tier is not a detail. Switching token abandons an attempt but leaves
it watched and payable, so the attempt that actually took the money is often older
than the last quote the buyer requested. Picking it here is what keeps these fields
agreeing with the
order.partial webhook, which reports the attempt the indexer
credited.400 with { "error": "Invalid request data" }. An id that is well-formed but does
not resolve — undecodable, unknown, or an order belonging to another store — answers
404 with { "error": "Order not found" }: an order you cannot see is reported as
missing, never as forbidden.
List orders
GET /v1/orders — requires orders:read. Newest first.
200 OK
Expire an order
POST /v1/orders/{id}/expire — requires orders:write.
Ends a pending order now rather than waiting out its window. This fires the
order.expired webhook and returns any reserved delivery stock to the pool, which
is the main reason to call it: an abandoned cart holding the last five licence keys is
worth reclaiming.
Returns 200 with the full order, now expired.
Expiring an order that a concurrent request already expired is not an error — you
get the expired order back, because your intent already holds.
Statuses
Two things that are easy to get wrong:
- Picking a coin does not change the order’s status. It creates a payment attempt
under the order, which stays
pending. Theorder.processingwebhook fires when the first attempt is created, as a “buyer engaged” signal — it is not a status. - An attempt that lapses with zero funds does not affect the order. The buyer can keep retrying with fresh quotes until the order itself expires.
Multi-item and currency rules
One currency per order
One currency per order
An order snapshots exactly one currency, taken from its products. Mixing a USD
product with a EUR product is
All items must use the same currency. If you sell
in both, keep separate carts.Duplicate products are merged
Duplicate products are merged
Sending the same
productId in two entries sums the quantities into one line
item. The response’s items array therefore may be shorter than the array you
sent.Prices are snapshotted
Prices are snapshotted
items[].title, unitPrice and image are captured at creation. Editing the
product later never rewrites a past order — which is what makes webhooks and
receipts trustworthy after the fact.Stock reservation is all-or-nothing
Stock reservation is all-or-nothing
Every line item that needs finite stock reserves it inside one transaction. If
any line cannot, the whole create fails with
Product is out of stock and
nothing is held. Reservations are released when the order expires.