Developers · Core concepts

Idempotency and retries

Make every write safe to retry with the Idempotency-Key header, and retry the right failures with the right backoff.

Version
v1.0.0
Base URL
https://dppapi-production.up.railway.app/v1
Format
JSON · OpenAPI 3.1
FeedbackLast updated
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 any POST or PATCH. 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 422 and idempotency_key_reused.
  • While the first request is still running, a concurrent retry with its key gets 409 idempotency_in_progress: wait briefly and retry.
  • A 5xx is never stored, so retrying after a server error really runs the request again. A 4xx is stored like any other answer.
  • Keys are 1–255 characters (otherwise 400 invalid_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

OutcomeRetry?How
Network error, timeoutYesSame request, same idempotency key.
429YesAfter retry-after seconds.
500, 502, 503, 504YesExponential backoff with jitter, same key.
409 idempotency_in_progressYesAfter a short wait, same key.
409 organisation_frozenNoContact us; the organisation is read-only.
400, 401, 403, 404, 422NoThe request itself must change.
207Only the failed rowsFix 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.

Cookie preferences

You choose which cookies are used. You can change your choice at any time with “Cookie settings” in the footer. Privacy policy

Necessary

Always on

Needed for the site to work: your light/dark theme and this cookie choice are stored in your browser. They do not track you.

Analytics

Google Analytics 4 measures how pages are used through statistics, which helps us improve the site. If you accept, it sets cookies and measures your visit in full. If you decline, it sets no cookies, but Google may still receive technical data such as the page viewed, your browser type and IP address.

_ga, _ga_* · up to 2 years · Google