Retry safety and order identity are different problems. Use Idempotency-Key so a network retry cannot double-fulfil the same HTTP mutation. Use orderReference so finance, support, and recovery can find your store order. Prefer both on every commercial write — changing either after the buyer pays breaks recovery.
Retry decision tree#
Example#
# On timeout after POST /orders, retry with the SAME Idempotency-Key and body
curl -s -X POST "https://api.avrix.io/api/seller/v1/orders" \
-H "Authorization: Bearer $AVRIX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: store-order-10432" \
-d '{"skuCode":"SANDBOX-ALWAYS-001","quantity":1,"orderReference":"store-order-10432","expectedUnitPriceCents":5999,"countryCode":"US","deliveryMode":"key"}'This page is the full contract for Idempotency-Key behaviour.
Who needs this#
Checkout and platform engineers implementing POST /orders (and other commercial writes) with safe retries.
TL;DR#
- Same
Idempotency-Key+ identical body → replay the cached successful response (no second side effect) for 24 hours. - Failed responses are never cached. After any 4xx/5xx, fix the cause and retry the same key — the request re-executes against live state.
- Same key + different body → 409 IDEMPOTENCY_KEY_MISMATCH.
- Never mint a new key to “force” a timed-out order — that risks a second fulfilment against the same charge.
- Keep a stable
orderReferenceas a secondary re-reveal safety net when quantity matches.
Two correlation layers#
| Mechanism | Role |
|---|---|
Idempotency-Key | At-most-once HTTP retry semantics for identical canonical JSON (24-hour window) |
orderReference | Business identity visible in aggregates, finance, and support (“find store-order-10432”), plus a secondary pull-safety net (below) |
They solve different problems. Prefer both on order flows.
| Scenario | Idempotency-Key | orderReference |
|---|---|---|
| Normal checkout | Stable per payment attempt (e.g. checkout session id) | Same store order id |
| Retry after timeout | Same key | Same reference |
| Second charge on a new payment | New key | New reference (or agreed suffix policy) |
Header rules#
| Rule | Detail |
|---|---|
| Format | 1–64 characters: [a-zA-Z0-9_-] |
| Required | On the routes listed below — omit → 400 BAD_REQUEST |
| Window | Honoured for 24 hours (cached successful response TTL) |
| Body match | Canonical JSON hash (key order and insignificant whitespace do not matter) |
| Replay header | Successful replays always include Idempotency-Replayed: true |
| Scope | Namespaced by endpoint + company + API key id — rotating or switching keys voids replay against the old key’s cache |
Claim lifecycle (in-flight)#
While a request holds the key, a concurrent retry with the same Idempotency-Key receives
409 IDEMPOTENCY_REQUEST_IN_FLIGHT and should honour Retry-After.
| Timing | Value | Meaning |
|---|---|---|
| Claim hold | 120 seconds | Maximum time an in-flight claim is considered live before another worker may take it over as stale |
Suggested Retry-After | 5 seconds | Returned on in-flight conflicts (and on some store-unavailable responses) |
Do not mint a new key while waiting — retry the same key and body after Retry-After.
Error-response replay vs release#
No error is ever cached. Only successful (2xx) responses are stored for replay. On POST /orders (and the same claim/commit pattern elsewhere):
| Outcome | Idempotency behaviour |
|---|---|
| Success (2xx) | Committed and replayed for the TTL |
| Any failure (4xx/5xx) — state-dependent gates (KYB_NOT_VERIFIED, TREASURY_CAPABILITY_MISSING, INSUFFICIENT_WALLET_BALANCE, 409 NO_AVAILABLE_KEYS), validation errors, transient 5xx | Claim is released — a retry with the same key re-executes against live state, so fixing the underlying cause (complete KYB, top up the wallet, restock) makes the same request succeed without rotating the key |
| Unhandled throw before commit | Claim is released in a safety net so the partner can retry |
Every failure path either has no side effects or compensates them (key quarantine, hold release), so re-execution is always safe. Failures observed on one attempt are re-evaluated — an error you received earlier is not proof of the current account state.
If the idempotency store cannot persist a final response after side effects ran, the API may return 503 IDEMPOTENCY_STORE_UNAVAILABLE — still retry with the same key.
Legacy rows without a body hash#
Cached rows that lack a stored request-body hash are treated as fail-closed:
409 IDEMPOTENCY_KEY_MISMATCH. They are not replayed, even if the body looks identical.
Only freshly written hashed entries are replayable.
Routes that require Idempotency-Key#
| Method | Path |
|---|---|
POST | /api/seller/v1/orders |
POST | /api/seller/v1/orders/bulk |
POST | /api/seller/v1/orders/hold |
DELETE | /api/seller/v1/orders/hold |
POST | /api/seller/v1/orders/reserve |
POST | /api/seller/v1/orders/commit |
POST | /api/seller/v1/orders/cancel |
POST | /api/seller/v1/keys/pull |
POST | /api/seller/v1/keys/export |
POST | /api/seller/v1/keys/return |
POST | /api/seller/v1/refunds |
POST | /api/seller/v1/webhooks |
Outcomes#
| Situation | Result |
|---|---|
| Same key, identical body, cached final response | Cached response replayed — Idempotency-Replayed: true — no second side effect |
| Same key, different body (or legacy row without hash) | 409 IDEMPOTENCY_KEY_MISMATCH with details.requestBodyHash |
| Same key, original still processing | 409 IDEMPOTENCY_REQUEST_IN_FLIGHT — honour Retry-After (typically 5s) |
| Idempotency store unavailable | 503 IDEMPOTENCY_STORE_UNAVAILABLE — fail closed; retry with the same key |
| Same key after a released validation error | New attempt proceeds (not a replay) |
Example mismatch#
{
"code": "IDEMPOTENCY_KEY_MISMATCH",
"message": "Idempotency-Key was already used with a different request body",
"details": {
"requestBodyHash": "a3f1c9..."
},
"requestId": "req_01J8XYZ...",
"doc_url": "https://docs.avrix.io/seller-api/idempotency"
}
Compare your outbound body’s canonical hash to details.requestBodyHash, or use a new key only when the mutation intent is genuinely new.
orderReference as secondary recovery#
orderReference is your store’s stable order id. On key pulls / POST /orders, when the same
allocation already has live pulled keys (revealed / assigned / redeemed) for that reference:
- If at least
quantitylive keys were already pulled for(allocation, orderReference), Avrix re-reveals those keys’ plaintext (no additional inventory consumption). Scope is per allocation — the same reference on a different allocation is a different pull. - If fewer than
quantitylive keys exist for that pair (partial prior fulfilment), the request is rejected with 409ORDER_REFERENCE_CONFLICT. There is no top-up: a largerquantitynever pulls additional keys under an existing reference. Use a neworderReferencefor a new sale. - If every previously pulled key for the pair was returned or refunded (zero live keys), a fresh pull is allowed — the prior delivery was fully compensated.
ORDER_REFERENCE_CONFLICT example#
{
"code": "ORDER_REFERENCE_CONFLICT",
"message": "orderReference already fulfilled 1 key(s); requested 3. Use a new orderReference for a new sale.",
"recoverable": false,
"hint": "Use a new orderReference for a new sale. A fully fulfilled orderReference on the same allocation replays the original keys; partial prior fulfillment cannot be topped up.",
"details": {
"recommendedAction": "fix_request_body_or_new_key",
"pullError": "ORDER_REFERENCE_CONFLICT"
},
"requestId": "req_01J8XYZ...",
"doc_url": "https://docs.avrix.io/seller-api/errors#order-reference-conflict"
}
Recovery preference
- Primary: retry with the same
Idempotency-Keyand identical body (HTTP idempotency). - Secondary: keep a stable
orderReferenceso a crash that lost the HTTP response (or used a new key by mistake) can still re-reveal already-pulled keys whenquantitymatches the original fulfilment.
Never invent a new orderReference for the same paid checkout. Never change quantity on a
recovery retry — replay requires the requested quantity to be covered by the keys already
fulfilled, and anything more returns 409 ORDER_REFERENCE_CONFLICT.
Safe retry pattern#
- Generate the key once when the buyer commits payment (not per HTTP attempt).
- Persist key + body +
orderReferencein your database before the first call. - On timeout /
5xx/ connection drop: retry with the same key and same body. - On
IDEMPOTENCY_REQUEST_IN_FLIGHT: wait forRetry-After, then retry same key/body. - On
IDEMPOTENCY_STORE_UNAVAILABLE: backoff and retry same key/body — never invent a new key. - On
IDEMPOTENCY_KEY_MISMATCH: stop — fix the client; do not “force through” with a new key against the same payment. - On 409
NO_AVAILABLE_KEYS: refund at your PSP — Avrix will not auto-refund. Failures are not cached, so a later retry with the same key is re-evaluated — it succeeds only if stock has actually been replenished.
Key rotation voids replay#
The replay namespace includes the API key id (see Header rules above). A retry sent with a different key — for example after a rotation — cannot see the cache written by the old key, so it executes as a brand-new request instead of replaying the cached response.
Rotation-safe retry recipe:
- Stop issuing new mutations with the old key, but keep the old key valid.
- Drain in-flight retries: wait out your full retry/backoff window (plus any queued retry jobs) so every retry of an old-key mutation still runs on the old key.
- Only then revoke the old key. See API key rotation.
- Never re-send an old-key mutation with the new key “to be safe”. On
POST /orderstheorderReferencebackstop protects against a double sale, but other mutations would execute a second time.
Choosing a key#
Good: checkout session id, PSP payment intent id, or a UUID minted once per logical mutation.
Bad: timestamps, random values per attempt, buyer email, or reusing one key across unrelated purchases.
Common mistakes#
- New
Idempotency-Keyon every HTTP retry of the same payment. - Reusing one key across unrelated purchases.
- Treating
orderReferenceas a substitute for HTTP idempotency on the first attempt. - Ignoring
Idempotency-Replayed: trueand double-applying side effects in your OMS.
Next steps#
- Key recovery runbook — the ordered procedure when a response was lost
- Creating orders
- Order lifecycle
- Error playbook
- Webhooks
- Glossary