Idempotency
Retry a request safely: how idempotency keys work, which endpoints accept them, and what the errors mean.
A request that times out or fails in transit leaves you without an answer: the card may have been issued, or it may not. Retrying blind risks issuing a second one. Send an idempotency key and the retry is safe — the first attempt's result comes back and the operation happens once.
How it works
Choose a unique string for each operation you want to protect and send it in the Idempotency-Key header:
cURL
curl --request POST \
--url https://api.themooncard.com/v2/api-gateway/cards \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Idempotency-Key: a1b2c3d4-0000-4000-8000-abcdef123456' \
--data '{
"cardholder_id": "3f8c6f2e-9c41-4a5f-9c73-0c9f4c0b7a11",
"account_id": "6c1c9a37-9c1b-4f6c-9e79-8f8a2f0d5f31",
"card_product_id": "8f1c611d-098d-4f61-b106-f7b6d344b1ae"
}'If that request completes, the card is created and you get your 201. If it times out and you send it again with the same key, you get the same 201 and the same card — the request is not performed a second time.
The key is yours to choose: any string up to 255 characters. A version 4 UUID is a good default. Generate it before the first attempt and reuse it for every retry of that same operation; a new operation needs a new key.
What is stored, and for how long
The status code and body of the first attempt are stored for 24 hours and replayed for any repeat with the same key. After that the key expires and the same value starts a fresh request.
Failures are stored too. If the first attempt returned 404 because a cardholder id was wrong, retrying with that key returns the same 404 rather than trying again. That is deliberate: a retry is meant to resolve "did my request happen?", not to produce a different outcome. Fix the payload and send it with a new key.
Two failures release the key instead of storing a result, because in both cases the operation never began:
- a request rejected by validation — correct the body and retry with the same key;
- a request rejected for authentication — the credentials can be restored and the request retried with the same key.
Which endpoints accept it
Every POST under /v1/api-gateway, /v2/api-gateway and /v1/public that changes state, with the exceptions below. Other methods ignore the header: GET and DELETE are already safe to repeat, and a PATCH or PUT here addresses a resource that already exists, so sending it twice converges on the same state without help.
Not accepted:
- The public endpoints that take no credentials: the two card-activation routes under
/v1/public/2faand cardholder login. A key is scoped to the caller, and these have no caller, so there is no namespace to scope it to. - The three document-upload endpoints (
…/verification-documents). A key must be claimed before the request is read, and the uploaded file is not available at that point — a key there could return the first document's response for a second, different document.
Endpoint pages show the header wherever it is accepted, so the reference is the authoritative list.
Keys are scoped to you
A key is namespaced to the organization that owns the API key. Your retry-1 and another integrator's retry-1 are different keys and never see each other's responses.
Errors
| Status | code | When |
|---|---|---|
400 | IDEMPOTENCY_ | The header is empty, blank, sent more than once, or longer than 255 characters. |
409 | IDEMPOTENCY_ | The key was already used for a different request. The message names the method and path it was first used with. |
409 | IDEMPOTENCY_ | The first request with this key is still being processed. Wait a few seconds and retry to receive its response. |
IDEMPOTENCY_ compares the whole request — method, path, query and body — so sending the same fields in a different order still counts as the same request. Anything else is treated as a new operation and needs a new key.
IDEMPOTENCY_ is the answer to a genuine race: two copies of the same request arriving at once. Exactly one executes; the other gets this and can retry for the stored result.
A key cannot get stuck in that state. If the request holding it never finishes — the connection died mid-flight, say — the key is released after 60 seconds and the next request with it starts fresh. That is longer than any call this API makes, so a request that is merely slow keeps its claim.
Writing a retry loop
- Generate a key before the first attempt and keep it with the operation you are retrying — in the row you are about to write, not in memory.
- Retry on a timeout, a connection failure, a
5xx, or a409 IDEMPOTENCY_. Back off between attempts.KEY_ IN_ PROGRESS - Treat any other
4xxas final: it is the answer, and it will be the answer for the next 24 hours. - Use a new key when you change the request.
Related
- Errors — the error envelope and the full code catalogue.
- Rate limits — how often you may retry.
Last updated August 18, 2026