Skip to main content

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

  • referenceId is required in POST /orders for real purchases (checkOnly: false).
  • It is unique per account (scoped to the API key).
  • Re-sending the same referenceId returns the original result with statusCode: 2 (IDEMPOTENCY_REPLAY). No second purchase is created — even if the request body differs.
  • For DTU validation (ordersType: "dtu" with checkOnly: true) referenceId is 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:

  1. Re-send the identical request with the same referenceId.
  2. If the order was created, you get it back with statusCode: 2.
  3. If it never landed, it is created now.

Either way you end up with exactly one order.