Skip to content

Idempotency

A network can drop the answer to a request that worked. To make a retry safe, every write carries an X-Idempotency-Key header, and a repeat of the same key gets the first answer back instead of running again.

  • Required on every write. POST /shipment-requests, PATCH /shipment-requests/{id}, POST /shipment-requests/{id}/cancel and POST /deals/{id}/documents without the header (or with one longer than 128 characters) answer 400 with Integration.IdempotencyKeyRequired.
  • 1 to 128 characters, chosen by you. Derive it from your own record, for example <record-id>-create, so a retry after a restart sends the same key.
  • Remembered for 24 hours, per key (the API key that made the call), method and path. A repeat within that time is not run again: the first answer comes back with the same status, body and ETag, and an X-Idempotency-Replayed: true header.
  • The request is compared. A key belongs to one request. The same key with a different request on the same route answers 422 with Integration.IdempotencyKeyReused, and nothing runs. “The same request” means the same JSON for a create, an update or a cancel: spacing, the order and the case of property names, the spelling of a number (5000, 5000.0 and 5e3 are one number) and the order of metadata keys do not matter; a value, including a metadata key’s case, does. For an upload it means the same file (by its content), the same category (in any case), the same file name and the same file type (parameters such as charset aside); the multipart boundary does not matter. Use a new key for a new request.
  • Only settled answers are kept. Successes and client errors (4xx) are replayed; a 5xx is not kept, so a retry after a server error runs again for real.
  • Not a verdict about the moment. A 412 (the request changed since you read it) and a 429 (an allowance ran out) are never kept: retry with the same key, after a fresh read or after the wait, and the retry really runs. Neither is an answer refused before the API read your request (a body that is not JSON or is too large, a missing scope): fix it and retry with the same key.
  • At the same moment. If a repeat arrives while the first call is still running, it answers 409 with Retry-After: 1. Retry after a second with the same key and you get the first call’s answer.
  • A daily allowance of new keys. Each API key can use 5,000 new idempotency keys per UTC day. Past it, a write answers 429 with Idempotency.DailyLimit and a Retry-After that runs to midnight UTC, and nothing runs: retry it after that, with the same key. A repeat of a key already answered that day is not counted and is replayed as usual.
  • An answer too large to keep whole. An answer over 256 KB, or one given after the key has used its daily storage for answers (8 MB, about 2,000 ordinary answers), is kept by its status only. A repeat still does not run again: it gets that status with X-Idempotency-Replayed: true and X-Idempotency-Body-Omitted: true, and no body. Read the record for the rest, for example GET /shipment-requests?externalReference=... after a create or GET /deals/{id}/documents after an upload.
HTTP/1.1 409 Conflict
Retry-After: 1
Content-Type: application/json
{ "status": 409, "title": "Conflict", "detail": "The same request is already being processed." }

Use one key per intended change: for an update, derive it from your record and the change (for example <record-id>-update-<n>), so the next change gets a new key. Other POST routes (the sandbox advance, the token endpoint) accept the header but do not require it.

Idempotency and externalReference together

Section titled “Idempotency and externalReference together”

The two protect against different things:

X-Idempotency-Key externalReference
Protects against The same HTTP call sent twice The same business record sent twice
Lasts 24 hours As long as the request exists
A repeat gets The first answer, replayed (X-Idempotency-Replayed: true) 409 with the first request’s requestId

So the safe retry after any failure is: the same X-Idempotency-Key and the same externalReference. If the first call created the request, you get its answer back (or a 409 naming it); no second request is ever opened. See externalReference.

The call log keeps a hash of the idempotency key with each call, never the key itself, so your Owner or Admin can match a retry to the call it repeated.