Developers
Idempotency & safe retries
Networks fail mid-flight. A retry must never create a second conversion or pay commission twice. This page explains how idempotency keys make write requests safe to retry, and how the platform also prevents double-processing at the domain level even when no key is sent.
Why idempotency matters
When your server sends a request and the connection drops before the response arrives, you do not know whether the operation ran. The only safe action is to retry — but a naive retry on an endpoint like POST /api/v1/conversions could report the same sale twice and pay an affiliate twice. Idempotency guarantees that sending the same request more than once has the same effect as sending it once.
Two independent mechanisms protect you. First, an optional idempotency key lets the API replay the original response for a repeated request instead of re-running it. Second, even without a key, the platform enforces uniqueness on the underlying domain records, so a duplicate is recognised and rejected as a no-op rather than processed again.
The idempotency key
Send the key in the X-Idempotency-Key header (the alias Idempotency-Key is also accepted). The header is optional — no endpoint requires it — but it is strongly recommended for every write you might retry. The key is a free-form string that you choose; it identifies one logical operation.
Which endpoints honor the key
The key is honored on the following write endpoints:
POST /api/v1/products/importPOST /api/v1/products/upsertPOST /api/v1/campaigns(create)POST /api/v1/conversionsPOST /api/v1/order-status- All
/v1/webhooks/*endpoints
Read-only and list endpoints (any GET), as well as product archive, are not idempotency-keyed — they are already safe to repeat or do not produce a replayable side effect.
What happens on a duplicate
The first request carrying a given key runs normally and its response (status code and body) is stored. The outcome of a later request with the same key depends on timing:
- Replay. A repeat that arrives after the first request finished, and within the 24-hour window, returns the stored response verbatim without re-running the operation.
- 409 Conflict. A repeat that arrives while the first is still processing, or that races to claim the key, returns HTTP
409. Wait briefly, then retry — once the original completes, the same key will replay its stored response.
The 24-hour window
A stored response is retained for 24 hours (IDEMPOTENCY_TTL_HOURS). Within that window, the same key replays. After it expires, the key is forgotten: reusing it would run the operation again. Because retries should resolve in seconds — not days — this window is comfortably large for any realistic retry loop.
A safe retry strategy
Retry only on transient failures: HTTP 5xx responses, connection timeouts, and network errors. Do not retry on 4xx (other than 409, which you retry after a short pause) — those indicate a request you must fix. Back off between attempts, and critically, reuse the same idempotency key and the same request body on every attempt. Changing either defeats the guarantee.
const KEY = 'ORD-1001-conversion'; // stable per logical operation
const body = JSON.stringify({
externalOrderId: 'ORD-1001',
clickId: 'clk_abc',
amount: 4999,
currency: 'NPR',
});
async function reportConversion() {
const maxAttempts = 5;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
let res;
try {
res = await fetch('https://api.example.com/api/v1/conversions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Idempotency-Key': KEY, // SAME key on every retry
// ...your HMAC auth headers
},
body, // SAME body on every retry
});
} catch (networkError) {
await backoff(attempt); // timeout / connection error -> retry
continue;
}
// 409: the first request is still in flight; pause and retry the SAME key.
if (res.status === 409) {
await backoff(attempt);
continue;
}
// 5xx: transient server error -> retry.
if (res.status >= 500) {
await backoff(attempt);
continue;
}
// 2xx (including 202 DUPLICATE) or a 4xx you must not retry: done.
return res;
}
throw new Error('conversion not confirmed after retries');
}
function backoff(attempt) {
const ms = Math.min(1000 * 2 ** (attempt - 1), 8000); // 1s,2s,4s,8s
return new Promise((r) => setTimeout(r, ms + Math.random() * 250));
}The same pattern in a shell loop with cURL:
KEY="ORD-1001-conversion" # SAME key for every attempt
for attempt in 1 2 3 4 5; do
code=$(curl -s -o /tmp/resp.json -w '%{http_code}' \
-X POST https://api.example.com/api/v1/conversions \
-H 'Content-Type: application/json' \
-H "X-Idempotency-Key: ${KEY}" \
--data-binary @conversion.json) # SAME body for every attempt
# 2xx including 202 DUPLICATE -> success
case "$code" in
2*) echo "ok ($code)"; cat /tmp/resp.json; break ;;
409|5*) sleep $((attempt * 2)); continue ;; # in-flight or transient -> retry
*) echo "client error $code, do not retry"; break ;;
esac
doneDomain-level deduplication
Idempotency keys are your first line of defence, but the platform also deduplicates on the meaning of each record, so double-processing is prevented even if a key is missing, expired, or differs between attempts:
- Conversions are unique per
(merchant + externalOrderId). A duplicate returns HTTP202with statusDUPLICATEand creates no second commission. - Webhooks are unique per
(merchant + X-External-Event-Id); re-delivering the same event id is ignored. - Order-status events dedupe on
(merchant, order, status[, statusUpdatedAt]), so re-sending the same transition is a no-op.
202 with status DUPLICATE — treat that exactly like a normal accept. Do not alert or retry on it; it confirms the original was already recorded.How to generate keys
A key must be stable per logical operation and identical across every retry of that operation — never random per attempt, or each retry would look like a new request. Derive it from your own domain identifiers so the same business event always maps to the same key:
ORD-1001-conversion— reporting the conversion for order 1001.ORD-1001-delivered— the “delivered” order-status update for order 1001.upsert-sku-100-2026-06-14— a daily product upsert for SKU 100.
Persist the key alongside the operation in your own system so that a worker resuming after a crash re-sends the same key. A random UUID generated at call time is an anti-pattern: on retry it produces a fresh key and the deduplication is lost.
Related
See Conversions for the conversion payload and its DUPLICATE response, and Webhooks for event-id deduplication and delivery retries.