Idempotency Keys: When Should Your API Use Them?

In short: Put an Idempotency-Key on POST and PATCH requests that create or change something the user would notice twice: an order, a payment, an invite, a ledger line. Retry a timeout with the same key and the same body. A new key is a new operation. GET, PUT, and DELETE are already idempotent, so a key on those methods does not buy you a second layer of safety.
What this means for you
- The button can be clicked twice, or the network can drop the response → key the POST.
- You are replacing one known record with PUT → the URL is the identity. A key is optional.
- You timed out and you are not sure the first call landed → retry the same key. Do not mint a new one.
The failure mode is ordinary. The client sends "create order." The server commits the row. The response never arrives. The client tries again. Without a key, you have two orders. Stripe's idempotent-requests guide exists for that case: the client generates a unique key, the server remembers the first result, and a retry with that key returns the saved response instead of performing the operation again.
Which methods already retry safely
MDN's method table splits this cleanly. GET, HEAD, OPTIONS, PUT, and DELETE are idempotent: the intended effect of sending the request twice is the same as sending it once. PUT replaces the resource at that URL. DELETE of a resource that is already gone is still "gone." POST and PATCH are not idempotent. A second POST can create a second resource. A second PATCH can apply the change again if your handler is "add 1" rather than "set the field to this value."
Stripe's API v1 docs match that split in practice: every POST accepts an idempotency key, and they tell you not to send one on GET or DELETE because it has no effect. The IETF Idempotency-Key draft is aimed at the same gap, POST and PATCH. It is an Internet-Draft, work in progress, not a finished standard. Treat Stripe's published behavior as the concrete design, and the draft as a description of the header and the error cases.
What the server should do with the key
| What arrived | What to do |
|---|---|
| First time you have seen this key | Run the operation. Store the key, a fingerprint of the body, and the response. |
| Same key, same body, first request already finished | Return the stored response. Do not create another row. |
| Same key, different body | Reject it. Stripe compares parameters and errors on a mismatch. The draft suggests 422 for a reused key with a different payload. |
| Same key, first request still running | 409 Conflict. Stripe documents 409 when another request with that key is executing. A new key here can start a second operation. |
| Key reused after you deleted it | It is a new request. Say so in your docs. |
Generate the key on the client, once per user action, and keep it until that action is resolved. Stripe suggests a UUID v4 or another random string with enough entropy to avoid collisions, up to 255 characters, and says not to use an email or other personal identifier as the key. If the app restarts between retries, the key has to be stored with the in-progress action. A key that lives only in memory is gone after a refresh, and the next attempt looks new.
Retries that are safe, and retries that are not
Stripe's error-handling guide separates three failures:
- Network error. You may not know whether the server ran the call. Retry with the same key and the same parameters. On Stripe API v1, limit that retry to the window while they still keep the key, which is at least 24 hours. After the key is gone, do not blindly send a new one. Look up the object, or the webhook, and reconcile.
- 409 while the first call is in flight. Wait and retry the same key. Minting a new key starts a second operation.
- You need to change the parameters. A cached 400 from a request that already started executing will replay if you send the same key. Errors that fail before execution, such as a bad API key, are not saved that way and can be retried after you fix them. If the body itself must change, confirm the original operation did not succeed, then use a new key.
Stripe API v2 is a different contract: a replay window of 30 days, and a failed attempt can be retried without repeating a success. If you copy a vendor's behavior, copy the version you actually call. Do not mix "always replay the first body, including 500s" (v1) with "retry the failed work" (v2).
Webhook delivery has the same shape from the other side. A provider may POST the same event twice. That is why handlers should be idempotent too. Choosing webhook versus polling is a separate decision, covered in webhooks vs polling. The key in this post is for the request your own client is retrying.
Where to start
- List POST and PATCH routes that create money, inventory, users, or messages. Those get keys first. A search endpoint does not.
- Require the header on those routes. Return a clear error when it is missing, so a client cannot "forget" and double-submit.
- Store the key, a hash of the body, the status code, and the response body. Unique on key. The draft calls the body check a fingerprint: the same key must not be accepted with a different payload.
- On a duplicate, return the stored response. On a different body, reject. On an in-flight duplicate, return 409.
- Document how long you keep keys. Clients should stop automatic retry when the window ends and reconcile instead.
How long the work takes depends on how many write endpoints you protect, where the key store lives, and whether each client can persist the key across a retry. That scope is set during discovery.
If the API is part of a product you are building, the contract belongs with the rest of the backend work: web and app development for the screens that must not double-submit, and Node.js development when the API itself is the thing you are shipping.
Frequently asked questions
What is an idempotency key?
It is a unique value the client sends so the server can tell a retry from a new operation. Stripe and the HTTP API working group's Idempotency-Key draft both put it in an Idempotency-Key request header. A UUID is the usual choice. Do not put an email address or other personal data in the key.
Which HTTP methods need an idempotency key?
POST and PATCH. MDN's method table marks those two as not idempotent, and marks GET, PUT, and DELETE as idempotent. Stripe's API v1 docs say not to send an idempotency key on GET or DELETE because it has no effect. PUT to a known URL replaces that resource, so repeating it does not create a second one.
What happens if the client retries with a new key?
The server treats it as a new operation. That is how a timed-out checkout becomes two charges or two orders. Stripe says to retry an unresolved POST with the same key and the same parameters. If you must change the body, first confirm the original operation did not succeed, then use a new key.
How long should the server remember a key?
Publish a window. There is no single duration that fits every API. Stripe's API v1 keeps a key for at least 24 hours and may delete it after that; a reuse after deletion runs as a new request. Stripe's API v2 uses a 30-day window and different replay rules. The HTTP draft says the server should document expiry if it has one. Match the window to how long your clients actually retry.
How long does it take to add idempotency keys?
It depends on how many POST or PATCH endpoints create something the user can be charged or duplicated for, where you store the key and the saved response, and whether mobile or browser clients persist the key across a retry. That scope is set during discovery. One payment endpoint is a different project from every write in an ERP.
Sources
- Stripe API — Idempotent requests — Clients send an idempotency key so a retry does not create a second object. Stripe suggests UUID v4, rejects a reused key with different parameters, and on API v1 does not apply keys to GET or DELETE.
- Stripe — Advanced error handling — Retry a network failure with the same key and parameters. A 409 means another request with that key is still running. API v1 retains keys at least 24 hours; API v2 uses 30 days and different replay behavior.
- MDN — HTTP request methods — GET, PUT, and DELETE are idempotent. POST and PATCH are not.
- IETF draft — The Idempotency-Key HTTP header (work in progress) — Defines an Idempotency-Key header for POST and PATCH. Recommends a UUID, says not to reuse a key with a different payload, and describes 409 while the first request is still in progress. This is an Internet-Draft, not an RFC.
Need help putting this into practice?
Tech Programmer builds and ships this work for startups and enterprises. Tell us what you are trying to do and we will tell you what it takes.
Related reading
- WebSockets vs Server-Sent Events: Which Real-Time Option Should You Use?Server-sent events (SSE) are a one-way HTTP stream: the browser opens an EventSource and the server pushes text events. WebSockets are a two-way session after an HTTP upgrade, so client and server can both send messages on the same connection. Prefer SSE for dashboards, notifications, logs, and AI token streams, and send user actions with ordinary HTTP requests. Prefer WebSockets for chat, collaborative editing, games, and other cases where the client sends continuously. Rare updates still belong on polling.
- gRPC vs REST: Which API Style Should You Use?REST exposes resources over HTTP with JSON — the default for browsers, public APIs, and third-party clients. gRPC is a remote-procedure-call system that uses Protocol Buffers and HTTP/2, with generated clients and built-in streaming. Prefer REST at the edge. Prefer gRPC for internal service-to-service calls when you own both sides and need a strict contract or streaming. Most products that need both keep REST outside and gRPC inside.
- JWT vs Session Authentication: Which Should Your App Use?Server-side sessions store a session ID in an HttpOnly cookie and look it up on every request — logout and bans take effect immediately. JWTs are signed, self-contained tokens the client sends back; the server verifies the signature without a session store. Prefer sessions for first-party browser apps. Prefer short-lived JWTs (plus server-stored refresh tokens) for mobile clients, public APIs, and microservices that cannot share a session store. Many products use both.