Idempotency
Network timeouts are not "did it happen?" mysteries here. Every purchase carries
a referenceId you choose, and the API guarantees at most one order per id.
The rule
referenceIdis required inPOST /ordersfor real purchases (checkOnly: false).- It is unique per account (scoped to the API key).
- Re-sending the same
referenceIdreturns the original result withstatusCode: 2(IDEMPOTENCY_REPLAY). No second purchase is created — even if the request body differs. - For DTU validation (
ordersType: "dtu"withcheckOnly: true)referenceIdis not required — nothing is bought.
:::warning A replay ignores your new body If you reuse an id with different items, you get the old order back, not a new one, and no error. Generate a fresh id for every distinct purchase — and reuse an id only when you are retrying that exact purchase. :::
Choosing an id
Use something you can reconstruct from your own database after a crash — that is the whole point. A row id, or a stable hash of (order, attempt):
order-2026-07-27-0001
tenant42:cart:9f2c1b
Random UUIDs generated at call time are fine for uniqueness but useless for recovery: if the process dies before it logs the id, you cannot tell whether the order exists. Persist the id before you send the request.
The safe purchase loop
ref = f"order-{db_order.id}" # 1. deterministic, persisted first
resp = client.create_order(reference_id=ref, check_only=False, orders=[...])
if resp.status_code == 2: # 2. replay: this is the original outcome
log.info("replay for %s", ref)
match resp.data.status: # 3. branch on the ORDER status
case "SUCCESS": mark_delivered(db_order)
case "IN_PROGRESS": schedule_poll(db_order)
case "CANCELLED": mark_failed(db_order, resp.status_message)
Notice what is not in there: no branching on the HTTP code. A replayed
cancelled order is an HTTP 200 with statusCode: 2 and
data.status: CANCELLED — three signals, and only the last one is about your
order.
For an asynchronous Shop purchase, the first response can be IN_PROGRESS
before Shop allocates an order. If Shop later proves that it refused that
attempt before allocating anything, polling returns CANCELLED and releases
the saved quote from the API key's transaction cap. Replaying POST /orders
with the same referenceId returns that cached cancellation with
IDEMPOTENCY_REPLAY; it never starts another purchase. Partial, ambiguous, or
malformed Shop results stay nonterminal so the API cannot cancel an order that
may exist.
After a timeout
You did not get a response. The order may or may not exist. Do not generate a new id:
- Re-send the identical request with the same
referenceId. - If the order was created, you get it back with
statusCode: 2. - If it never landed, it is created now.
Either way you end up with exactly one order.