Idempotency
Retry any POST safely. The Idempotency-Key header turns "did my request go through?" into a non-question.
How it works
Every POST /api/* request must carry an Idempotency-Key header. The
first attempt is processed and its response stored. Any retry with the
same key and the same request replays the stored response byte for
byte, marked with an idempotency-replayed: true header. No side effects,
no duplicates.
curl -s $BASE/api/customers \
-H "Api-Key: $KEY" \
-H "Idempotency-Key: 4f1f86f0-9e37-4c8e-9f2a-1f2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{"kind": "individual", "first_name": "Ana", "last_name": "Silva",
"tos_acceptance": {"ip_address": "203.0.113.7"}}'
Generate a fresh key, ideally a UUID, for every new request. Reuse a key only to retry that exact request.
The rules
| Situation | Result |
|---|---|
| Same key, same request, within 24 hours | Stored response replayed, with idempotency-replayed: true |
| Same key, different body or different endpoint | 422 idempotency_key_reused, and the message names where the key was first used |
| Same key, first attempt still running | 409 request_in_progress. Wait and retry |
| First attempt returned 5xx | Not stored. A retry executes fresh |
| Key older than 24 hours | Forgotten. The request executes as new |
| Missing key on a POST | 400 missing_idempotency_key |
Keys are global per environment, not per endpoint. Using
Idempotency-Key: test on one endpoint and then on another fails loudly,
with a message like "This Idempotency-Key was already used for
POST /api/tos_links". That is deliberate: it catches key-management bugs
instead of silently replaying an unrelated response. Always mint a unique
key per request.
Idempotency below the header
The header protects your retries. The platform is idempotent underneath as well, so replays cannot create duplicates at any layer:
- A replayed bank callback cannot create a second deposit.
- Re-uploading the same verification document is detected by content hash and skipped.
- Ledger postings carry deterministic keys, so a retried internal job can never double-post.
- A wallet operation that is already executing replays its object rather than authorizing twice.
You do not need to do anything to benefit from these. They are listed so you can reason about failure modes with confidence.
The one exemption
The hosted terms acceptance page (/api/tos/accept) is exempt, because a
browser form cannot send an Idempotency-Key. It is idempotent by
construction instead: refreshing or double-clicking replays the same
receipt. Every other /api/* POST requires the header.