Idempotency
Order creation is safe to retry
Section titled “Order creation is safe to retry”Every order-create endpoint (POST /api/v1/orders/topup, /esim, /scent) takes an
externalReference field — your own idempotency key. If a create request times out, or you
simply don’t know whether your first attempt landed, retry it with the exact same
externalReference: you will get back the original order, never a duplicate, and never a second
charge.
This holds even under real failure conditions your integration can’t control — a dropped
connection after the server already committed the order, a retry racing a still-in-flight first
attempt, or a network partition mid-request. The one thing you must not do is generate a new
externalReference for what is logically the same business action; that genuinely creates a new
order.
// Attempt 1 (response never received due to a network timeout)POST /api/v1/orders/scent{ "partnerId": "...", "externalReference": "po-2026-04-1029", "...": "..." }
// Attempt 2 — safe. Same externalReference, retried after the timeout.POST /api/v1/orders/scent{ "partnerId": "...", "externalReference": "po-2026-04-1029", "...": "..." }// -> returns the SAME order both times, whichever attempt actually created it.A concrete guarantee worth knowing about, not just relying on: while an order with a given
reference is still being created (a rare, narrow window), a retry gets 409 CONFLICT_ORDER_IN_PROGRESS — retry again shortly. Once the order reaches a terminal state, a
retry with the same reference always returns the real, current order data.
Why there’s no separate cart-level idempotency key
Section titled “Why there’s no separate cart-level idempotency key”You might expect POST /api/v1/scent/cart/items or the checkout flow to need its own idempotency
key too. It deliberately doesn’t, for three reasons:
- Cart mutations are naturally idempotent-ish by merge semantics. Adding the same variant twice merges into one line’s quantity rather than creating a duplicate line — retrying an “add item” call is safe on its own terms.
- The checkout flow has a mandatory preview step. You always call
GET /api/v1/scent/cart/checkout-previewbefore creating the order — see Cart preview & price locks — so a retried preview simply re-prices the same cart; nothing is committed until the order-create call. - The actual money-moving step — order creation — already has bulletproof idempotency (above). Since the cart itself never charges anything, adding a second idempotency layer in front of it would protect a step that has nothing to protect.
If you’re building a retry wrapper around your integration, the practical rule is: retry cart
mutations and previews freely (they’re safe by construction); retry order-create with the same
externalReference every time for the same business action.