Skip to content

Idempotency

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:

  1. 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.
  2. The checkout flow has a mandatory preview step. You always call GET /api/v1/scent/cart/checkout-preview before 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.
  3. 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.