Request header · non-standard
> Idempotency-Key: "8e03978e-40d5-43e8-bc93-6894a57f9324"Idempotency-Key Header: Safe POST Retries
Idempotency-Key lets clients retry POST and PATCH without duplicating work. The IETF draft, Stripe behavior, and how to design the server side.
- Direction
- Request
- Category
- API design
- Status
- Non-standard: Internet-Draft, not an RFC
- JS can set it
- Yes
- CORS-safelisted
- No: may trigger a preflight
On this page
TL;DR: The client generates a unique
Idempotency-Keyper logical operation and resends the same value on every retry. The server stores the key with a fingerprint of the request and the response, and replays that response instead of repeating the side effect. It is an IETF draft, not an RFC.
The problem it solves
A client sends POST /payments, the connection drops after the server committed the charge, and the client never sees a response. Retrying a POST is unsafe because POST is not idempotent (RFC 9110 section 9.2.2). PUT and DELETE are, so retrying them is fine. Idempotency-Key gives POST and PATCH the same safety.
POST /v1/payments HTTP/1.1
Host: api.example.com
Content-Type: application/json
Idempotency-Key: "8e03978e-40d5-43e8-bc93-6894a57f9324"
{"amount": 4200, "currency": "usd", "customer": "cus_123"}
The draft defines the field as an Item Structured Header whose value must be a String (RFC 8941 section 3.3.3), which is why its examples show the value in double quotes. Stripe’s docs show it unquoted (Idempotency-Key: KG5LxwFBepaKHyUD). If you build a server, decide which forms you accept and document it. The draft recommends a UUID or similar random identifier.
Status of the specification
The current document is draft-ietf-httpapi-idempotency-key-header, revision 07 (October 2025, authors J. Jena and S. Dalal), a Standards Track draft of the HTTPAPI working group. The datatracker marks it expired as of April 2026. Cite it as work in progress and do not depend on details such as quoting rules staying fixed.
Status codes the draft recommends
- Key required but missing:
400 Bad Request. - Same key, different request payload:
422(Unprocessable Content). - Retry arrives while the original is still running:
409 Conflict.
Real APIs vary. Stripe documents that its idempotency layer errors when parameters differ from the original request but does not tie that to 422, so follow your own documentation and pick one convention across all endpoints.
Server design
Store one record per (account, key):
CREATE TABLE idempotency_keys (
account_id bigint NOT NULL,
key text NOT NULL,
fingerprint bytea NOT NULL, -- hash of method, path, body
status text NOT NULL, -- 'in_progress' or 'completed'
response_code int,
response_body bytea,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (account_id, key)
);
The handling sequence:
- Insert the row as
in_progress, letting the primary key do the locking. If the insert conflicts, the key was seen before. - Seen before and
in_progress: return409, ideally withRetry-After. - Seen before and the fingerprint differs: return
422. Never run the new payload under an old key. - Seen before,
completed, fingerprint matches: return the stored status and body without executing anything. - New key: run the operation, then write the response and mark
completed, in the same transaction as the side effect when you can.
Scope keys per authenticated account, otherwise one tenant can collide with or probe another’s keys. Fingerprint the method, path and canonicalised body, not volatile headers. Expire rows on a documented schedule; the draft says the server should publish its expiry policy. Stripe’s is 24 hours.
A crash between executing the side effect and recording the response leaves an in_progress row forever. Give those rows a lease (a locked_until timestamp) so a later retry can take over, and make the underlying operation itself safe to resume, for example by passing the same key to the downstream payment provider.
Client rules
- Generate the key once per logical operation, before the first attempt, and persist it so a crash and restart reuses it.
- Reuse the key only for byte-identical retries. A changed amount needs a new key.
- Do not put emails or other personal data in the key; Stripe gives the same advice.
Verify
KEY=$(uuidgen)
curl -si https://api.example.com/v1/payments -H "Idempotency-Key: $KEY" -d '{"amount":4200}'
curl -si https://api.example.com/v1/payments -H "Idempotency-Key: $KEY" -d '{"amount":4200}' # replayed
curl -si https://api.example.com/v1/payments -H "Idempotency-Key: $KEY" -d '{"amount":9900}' # mismatch
Related
Frequently asked questions
Is Idempotency-Key an official HTTP standard?
No. It is an Internet-Draft from the IETF HTTPAPI working group (draft-ietf-httpapi-idempotency-key-header). At the time of review the latest revision is -07, dated October 2025, and the datatracker lists it as expired and archived, so treat it as a convention rather than a published RFC. Payment APIs such as Stripe use the header name in production regardless.
How long does Stripe keep idempotency keys?
Stripe documents that keys can be removed from the system automatically once they are at least 24 hours old. A key reused after it was pruned is treated as a new request. Keys are up to 255 characters long.
Which status code for a reused key with different parameters, 409 or 422?
The draft says the server SHOULD reply 422 when a key is reused with a different request payload, and 409 when a retry arrives while the original request is still being processed. A missing key on an endpoint that requires one gets 400.
Do GET and DELETE need an Idempotency-Key?
No. They are idempotent by definition in RFC 9110 section 9.2.2, and Stripe states that sending keys on them has no effect. The header exists for POST and PATCH.
Should a failed request be replayed from the stored result?
Stripe saves the status code and body of the first execution, including 500 errors, and replays them for the same key. It does not save a result when validation fails before execution starts or when the request collided with one in flight, because nothing ran; those can be retried freely.
Sources
Related
Idempotent
Learn what idempotent means in HTTP. Understand why GET, PUT, and DELETE are idempotent, why POST is not, and how idempotency affects API design.
HTTP PATCH Method
Learn how HTTP PATCH requests apply partial modifications to resources. Understand JSON Patch, merge patch formats, and when to use PATCH vs PUT.
HTTP POST Method: Complete Guide with Examples
Learn how the HTTP POST method works. Understand when to use POST requests, request bodies, form submissions, and API calls with practical examples.
409 Conflict
409 Conflict means the request clashes with the resource's current state: duplicates, stale versions, locks. Use ETags and If-Match, and handle retries.