Este artículo aún no está disponible en español; se muestra a continuación en inglés.
On this pageThe Idempotency-Key header
Networks fail in the worst place: after the server has done the work and before you hear about it. An idempotency key makes the retry return the original answer instead of doing the work twice.
The Idempotency-Key header
- Send
Idempotency-Key: <unique string>on anyPOSTorPATCH. A UUID v4 is ideal. - The first response — success or error — is stored for 24 hours against your API key and that key.
- A retry with the same key and the same request (method, path and body) gets the stored response back — same status, same body — with the header
Idempotent-Replayed: true. Nothing is executed a second time. - The same key for a different request is refused with
422andidempotency_key_reused. - While the first request is still running, a concurrent retry with its key gets
409idempotency_in_progress: wait briefly and retry. - A
5xxis never stored, so retrying after a server error really runs the request again. A4xxis stored like any other answer. - Keys are 1–255 characters (otherwise
400invalid_idempotency_key) and scoped to the API key that sent them, so two integrations cannot collide. - Dry runs are answered before idempotency is consulted: they are never stored or replayed.
A retry-safe batch
curl -X POST "https://dppapi-production.up.railway.app/v1/items/batch" \-H "Authorization: Bearer $DPP_API_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: 7b3e0c52-1f7d-4b8a-9a52-2f0e6d1c9a44" \-d '{"modelId": "cm1q8x2k40003l70f9a8s7d6e","count": 100,"serialPrefix": "C48-2027-","placeOfManufacture": "Stuttgart, Germany"}'422 · same key, different body
{"error": "idempotency_key_reused","request_id": "3f1c2a9e-8d4b-4c6f-9e2a-7b5d1c0e8f34"}Choosing keys
Derive the key from the operation, not the attempt. Generate it once when the work is decided and keep it with the job, so a retry after a crash reuses it. Good keys are a UUID stored with the job, or a natural id: batch-<ERP order>-<line>, publish-<lot>-2027-01-15.
What to retry
| Outcome | Retry? | How |
|---|---|---|
| Network error, timeout | Yes | Same request, same idempotency key. |
429 | Yes | After retry-after seconds. |
500, 502, 503, 504 | Yes | Exponential backoff with jitter, same key. |
409 idempotency_in_progress | Yes | After a short wait, same key. |
409 organisation_frozen | No | Contact us; the organisation is read-only. |
400, 401, 403, 404, 422 | No | The request itself must change. |
207 | Only the failed rows | Fix and resend just those, with a new key. |
A retry helper
import { randomUUID } from 'node:crypto';const RETRYABLE = new Set([429, 500, 502, 503, 504]);/** POST with one idempotency key across every attempt, backing off between them. */async function postWithRetry(url, body, { attempts = 5 } = {}) {const key = randomUUID(); // one key for the logical operation, not per attemptfor (let attempt = 1; ; attempt++) {let res;try {res = await fetch(url, {method: 'POST',headers: {Authorization: `Bearer ${process.env.DPP_API_KEY}`,'Content-Type': 'application/json','Idempotency-Key': key,},body: JSON.stringify(body),});} catch (networkError) {if (attempt >= attempts) throw networkError;}if (res && !RETRYABLE.has(res.status)) return res;if (attempt >= attempts) return res;const retryAfter = Number(res?.headers.get('retry-after'));const backoff = retryAfter > 0 ? retryAfter * 1000 : Math.min(30_000, 2 ** attempt * 250) * (0.5 + Math.random());await new Promise((r) => setTimeout(r, backoff));}}Naturally idempotent operations
- Publishing an already-published passport returns it unchanged (bulk publish reports
alreadyPublished). PATCH /models/{id}with the same fields leaves the same values.- Recording an event is not naturally idempotent — the same reading sent twice is two events. Use a key.