Skip to content

Idempotency ​

Networks fail after the server committed. Idempotency-Key is how you retry anyway.

http
POST /api/public/v1/programs/prg_01J9…/cards HTTP/1.1
Authorization: Bearer tsk_live_dP4y…
Idempotency-Key: issue-card:EMP-00418
Content-Type: application/json

{ "external_ref": "EMP-00418", "holder": { "name": "A. Rossi", "email": "a.rossi@example.com" } }
  • Accepted on every POST, PATCH and DELETE. CRMs retry; the header is there for all of them, not only for card issuing.
  • A request without the header is processed normally, not rejected. You simply have no safe retry — and after a timeout you cannot tell whether the card was issued.
  • A repeat with the same key and the same body replays the stored response, including its status code. The write happens once.
  • The same key with a different body is a 409 idempotency_conflict. See below.
  • Keys are remembered for a window shown in Rate limits and quotas. After that a repeat is a new request.

Choosing a key ​

Any string you can regenerate for the same logical operation. A UUID per operation is fine. Deriving it from your own record is better — a job that crashes and restarts regenerates the same key and stays idempotent:

issue-card:EMP-00418
update-card:cin_01J9…:2026-09-04T10:15:00Z

Do not reuse a key across different operations, and do not use a constant.

The 409, in full ​

409 idempotency_conflict means one of exactly two things, and the message distinguishes them:

1. The key was already used with a different request body.

json
{ "error": { "code": "idempotency_conflict",
             "message": "Idempotency-Key already used with a different request body",
             "request_id": "req_01J9…" } }

This is a caller bug and retrying cannot fix it. Something in your body changed between attempts — a re-serialisation that reordered keys, a timestamp recomputed inside the retry loop, a "last modified" field that ticked. Serialise the body once, outside the retry loop, and reuse those bytes.

2. The key was claimed by a request that never recorded a response.

json
{ "error": { "code": "idempotency_conflict",
             "message": "Idempotency-Key claimed by a request that did not complete; retry with a new key",
             "request_id": "req_01J9…" } }

An earlier attempt acquired the key and did not finish. Start over with a new key. Whether the original write landed is answerable by reading — search cards by your external_ref before you re-issue.

The retry loop, written and tested ​

Do not write this from scratch. The integration kit has it in Node, PHP and Python, executed by our test suite against the real error shapes:

  • 2xx → done;
  • 429 → honour Retry-After, same key;
  • 5xx and network errors → exponential backoff, same key;
  • 409 idempotency_conflict → do not retry, surface it with the request_id;
  • everything else → a decision, not a retry.

The one thing that is not idempotent ​

Reading. GET requests take no idempotency key because they change nothing — retry them freely.

Requires the Business API add-on.