Skip to content
Linra Omni Developer Docs

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:

  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/{customerRef}/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, 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).