Idempotency
Order-create's idempotency guarantee, and why cart operations don't need one.
Order creation is safe to retry — but a retry after success is a REJECT, not a replay
Section titled “Order creation is safe to retry — but a retry after success is a REJECT, not a replay”POST /api/v1/scent/orders 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 NEVER get a duplicate order and you will NEVER be charged twice —
that guarantee is unconditional and enforced at the database level (the (partnerId, externalReference) pair is unique). 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.
What changes depending on WHEN the retry lands:
| The first attempt’s state when you retry | What the retry gets |
|---|---|
| Still being processed (a narrow, in-flight window) | 409 CONFLICT_ORDER_IN_PROGRESS — retry again shortly. |
| Already reached a terminal FAILURE (compensated/rolled back) | A genuinely fresh attempt — a new order may be created. |
| Already succeeded | 409 CONFLICT_ORDER_DUPLICATE_REFERENCE — not a replay of the original order. |
// Attempt 1 (response never received due to a network timeout, but it landed server-side)POST /api/v1/scent/orders{ "externalReference": "po-2026-04-1029", "...": "..." }
// Attempt 2 — same externalReference, retried after the timeout.POST /api/v1/scent/orders{ "externalReference": "po-2026-04-1029", "...": "..." }// -> 409 CONFLICT_ORDER_DUPLICATE_REFERENCE// { "state": "CONFLICT_ORDER_DUPLICATE_REFERENCE",// "payload": null,// "details": { "externalReference": "po-2026-04-1029", "orderId": "5b1e3a1c-..." } }The database-level uniqueness (no duplicate order, no double charge) was never in question here —
what changed is the SURFACE behavior on the success path: a repeat call against an order that
already completed used to transparently return the original order’s data; it now REJECTS with
409 CONFLICT_ORDER_DUPLICATE_REFERENCE instead. Recover by reading details.orderId from the
error and calling GET /api/v1/scent/orders/{orderId} — that orderId is exactly the id the original,
successful create would have returned as its own id. A retry loop that only checks the HTTP
status and gives up on a non-2xx will miss its own order — always inspect details.orderId on
this specific error code before treating a retry as failed.
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/{customerRef}/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/{customerRef}/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, and treat a 409 CONFLICT_ORDER_DUPLICATE_REFERENCE response as success — read details.orderId and fetch the
order, don’t surface it to the customer as a failure.
Webhook deliveries have their OWN idempotency mechanism: eventId
Section titled “Webhook deliveries have their OWN idempotency mechanism: eventId”Everything above covers requests you send to the API. Webhook deliveries run the opposite
direction — Linra calling your endpoint — and have a completely separate idempotency
mechanism: every delivered event carries an eventId, a stable, opaque dedup token. Delivery is
at-least-once, so your webhook handler must be idempotent on eventId (store the ids you’ve
already processed and skip a repeat) rather than assuming each event arrives exactly once. See the
Webhooks & HMAC verification guide for the full envelope
shape, samples, and why eventId is deliberately opaque (never parse it for an order id or a
timestamp — read orderId inside data instead).

