Idempotency in REST API Design and Documentation
Distinguish effect from response, document clearly, and idempotency stops being a costly surprise.

Idempotency in REST API design comes down to one distinction most engineers absorb wrong the first time: whether repeating a request changes the outcome, versus whether it changes the response you get back. Get that right and document it clearly, and retries become boring, which is the whole point. Get it wrong, or leave it undocumented, and you find out about double-charged customers from an angry support ticket, not a test suite.
RFC 9110, which replaced RFC 7231 as the governing HTTP standard, defines it precisely: a request method is idempotent if the intended effect of multiple identical requests matches the effect of a single one. Effect, not response. I've watched that word choice trip up otherwise sharp engineers more times than I can count, and it's the part most tutorials skate past on their way to a code sample.
Take DELETE. Call it once against a resource that exists, you get a 200. Call it again, you get a 404, because the resource is already gone. Different status codes, same underlying truth: the resource is deleted either way, and server state didn't move between call one and call two. Only the description of that state changed. Confusing "same effect" with "same response" is probably the single most common misunderstanding in this corner of API design, and it catches developers who otherwise know the spec cold.
RFC 9110 also draws a boundary around what idempotency actually promises, and it's narrower than people assume. A server can log every request separately, keep a full revision history, fire off internal metrics, run whatever non-idempotent side effects it wants behind the scenes. Idempotency governs the effect on the resource the client asked about. Nothing else.
Safety is a different property, worth pulling apart from idempotency rather than letting the two blur together. Safe methods, GET, HEAD, OPTIONS, TRACE, don't touch state at all; they just read. Every safe method is idempotent by definition, but the reverse doesn't hold. PUT and DELETE change state, yet they're still idempotent, because repeating them lands you in the same place every time. Conflating "safe" with "idempotent" leads people to assume they can retry anything that doesn't look obviously destructive on its face. This assumption gets expensive fast, and I've seen the invoice.
How each HTTP method maps to idempotency, and why POST and PATCH are the exceptions
The spec sorts methods into two camps. Idempotent by design: GET, HEAD, OPTIONS, TRACE, PUT, DELETE. Not idempotent by default: POST and PATCH. They fail the test for genuinely different reasons.
POST creates something new on every call, typically. Send the same "create an order" request three times without protection, and you get three orders, three shipping notifications, maybe three billing events. That multiplication isn't a bug you patch later. POST was never promised to be repeatable in the first place, so it's doing exactly what it was built to do.
PATCH is the subtler one, and the one that actually causes arguments in design reviews. It applies a partial update, and whether that update is idempotent depends entirely on what kind of instruction it carries. "Set status to shipped" is idempotent; run it ten times and the resource lands in the same state every time. "Increment inventory count by 1" behaves completely differently; run it ten times and you've added ten units instead of one, because each execution depends on the resource's current state rather than an absolute target.
PUT sidesteps this trap by design. It replaces the entire resource with a known payload, so it doesn't matter what the resource looked like before the call; the result is fully determined by what you sent, full stop. PATCH describes a delta, and a delta only behaves idempotently when it's expressed as an absolute value rather than a relative one. That's a design choice, not an accident of the protocol, and it shows up constantly in how teams structure update endpoints, usually after someone's been burned once.
Notice which methods land on the "not idempotent" side. POST and PATCH are exactly the ones most likely to touch money, send a message, or create a permanent record. Not a coincidence. That's why idempotency engineering exists as its own discipline instead of a footnote in the HTTP spec.
Where non-idempotent calls cause real damage, and why networks make it unavoidable
Network failures aren't the exception in distributed systems. They're the baseline condition you design around, whether you like it or not. A client sends a request, the connection drops before the response arrives, and the client has no way to know whether the server actually processed it, so it retries. That instinct is correct. Whether it's safe depends entirely on whether the endpoint on the other end is idempotent.
Payment processing is the example everyone reaches for, and it earns the reputation honestly. A charge request times out, the client retries assuming the first attempt failed, and now the customer's been billed twice for one purchase. The damage isn't abstract here, and it isn't delayed; it shows up on a statement within minutes and costs trust in a way an apology email doesn't fully repair.
Distributed systems make this worse, since failures compound across service boundaries rather than staying contained. One non-idempotent operation buried in a longer pipeline, say an order service calling a payment service calling a notification service, gets amplified at every hop downstream. Idempotency, applied correctly, gives the system a way to absorb these failures gracefully: a request that times out, gets retried, and eventually succeeds leaves no trace of the intermediate failure in the resource's final state.
There's a secondary benefit worth mentioning, though it's not the main event here. Safe and idempotent methods like GET can get cached by CDNs and intermediate proxies, cutting server load and shaving latency. Nice bonus. But performance is secondary to the actual reason idempotency matters: correctness under failure. Speed just comes along for the ride once the design is right.
So documentation stops being optional at this point. If a developer consuming your API doesn't know whether an endpoint is safe to retry, they're stuck choosing between two bad options: retry blindly and risk duplication, or refuse to retry and risk leaving the system in a broken intermediate state after a legitimate transient failure. A design guarantee nobody wrote down isn't really a guarantee. Call it a coin flip wearing an API contract as a costume.
Idempotency keys: how to make POST and PATCH safe by design
Since POST and PATCH can't be made idempotent through method semantics alone, the industry landed on a mechanism that bolts idempotency on as a layer: the idempotency key. The client generates a unique value, attaches it via an Idempotency-Key header, and the server stores that key alongside the result of the first execution. Any later request carrying the same key gets the original result replayed back instead of triggering a second execution.
Key generation matters more than it sounds like it should. The standard recommendation is a V4 UUID, or an equivalent random string with enough entropy that collisions are, for practical purposes, impossible. Stripe's documentation specifies a 255-character limit on key length and warns explicitly against encoding sensitive information, email addresses, personal identifiers, directly into the key string. Small detail. Real security implications if you ignore it.
Expiration is the part teams underthink, in my experience almost every time. Keys need to live long enough to cover a realistic retry window, then get cleared out so storage doesn't grow forever. Stripe's v1 API uses a 24-hour window; v2 extended that to 30 days, which tells you something honest about how the company's own operational scars reshaped the design over time. Some retry scenarios genuinely need days, not hours. The right window depends on what the API does: payment retries might need to survive a long weekend, while a real-time event API might only need a few minutes of coverage before a stale key becomes meaningless anyway.
Where you store these keys matters just as much as how long you keep them. The store needs to be fast, since checking it happens on the hot path of every request, and durable, since losing keys on a restart defeats the entire point during exactly the retry windows where it matters most. An in-memory cache is quick but forgets everything the moment the process restarts, which turns a routine deploy into a correctness bug nobody saw coming. Redis tends to be the default choice: fast, supports persistence, has expiration built in natively instead of bolted on after the fact.
One more thing worth being deliberate about, and it's easy to skip: scoping. A key needs to be tied to a context, at minimum the authenticated user and the specific endpoint, or you risk collisions across tenants in a multi-tenant system. Two different customers generating the same UUID by pure chance is astronomically unlikely. Two different customers whose client libraries generate keys with insufficient randomness, though, is a real failure mode, and one that's shown up in production before.
What the server must do when it receives a duplicate, a conflict, or a failed original
The straightforward case: a duplicate request arrives with a recognized key, and the server returns exactly what it returned the first time, same status code, same body, even if that first response was an error. Replay means replay, errors included, not just successes.
There's a nuance here that breaks naive implementations constantly, and it's the one that gets missed most often. Caching only starts once the request has actually reached endpoint execution. If a request fails validation before it gets that far, a malformed payload, a missing required field, that failure never gets cached against the key, and retrying with the same key makes the server attempt execution again from scratch. A lot of retry logic assumes any response tied to a key, including a 400, gets replayed automatically. Building on that assumption means silently re-attempting validation failures forever.
What happens if the same key shows up with a different payload? The server should compare the incoming request against what it stored originally and return a 409 Conflict if they don't match. This catches genuine misuse, a client accidentally reusing a key across two unrelated operations, before it can quietly corrupt data.
Concurrency adds another wrinkle. If a second request lands with the same key while the first is still processing, the correct response is 409 Conflict, signaling that the original is already in flight. Adyen's live implementation returns either 409 or 422 with error code 704 specifically for this case, and the IETF draft standard now codifies the same pattern. This is the kind of detail that shows up only in documentation written by people who've debugged this exact failure mode in production themselves, under pressure, with a clock running.
After a permanent 4xx failure, the client needs to generate a fresh key rather than retry with the old one. The original key is now permanently bound to the failed result, and replaying it only ever hands back the same failure, forever. As for retry strategy on the client side, the first retry can be fast, since a lot of failures are just transient blips, but later retries should back off exponentially with jitter added in, so a recovering service doesn't get hammered by every client retrying on the same schedule at once.
The emerging IETF standard for the Idempotency-Key header and what it formalizes
The IETF's "Building Blocks for HTTP APIs" working group has been formalizing the Idempotency-Key header, currently sitting at draft revision 07 as of the 2025-2026 cycle. What matters here is which direction the standardization is flowing.
The draft largely codifies what production APIs had already converged on independently, without anyone coordinating. The key must be unique and must never get reused against a different request payload. A UUID per RFC 4122, or an equivalent random identifier, is the recommended format. An optional idempotency fingerprint can travel alongside the key to verify payload consistency server-side, formalizing the 409-on-mismatch behavior Stripe and others were already running ad hoc, years before anyone wrote it into a spec.
What makes this draft worth watching is that it explicitly traces its lineage back to Stripe's and PayPal's production documentation. Standards bodies more commonly define behavior first, with industry catching up afterward. Here the sequence ran backward: real production systems figured out what worked at scale, documented it publicly, and the IETF is now writing that hard-won practice into a formal spec.
The draft also calls for Idempotency-Key to be added permanently to the IANA HTTP Field Name Registry. Once finalized, the header graduates from a vendor-specific convention that happens to be widely copied into a first-class HTTP header with the same standing as Authorization or Content-Type. For anyone designing an API today, the draft is mature enough to build against right now, and aligning with it early avoids breaking changes down the line.
How Stripe, Adyen, and other production APIs have documented their idempotency contracts
Stripe's documentation is a useful reference point, and for good reason. It states plainly which methods accept the Idempotency-Key header, POST in v1, POST and DELETE in v2. It specifies the replay windows directly, 24 hours in v1, 30 days in v2, right in the API reference where a developer sees it while actually building something rather than digging through a changelog. It documents the 255-character key limit and the warning against embedding sensitive data. It spells out what "replay" means in practice: same status code, same body, errors included. And when a request fails with a 4xx, the documentation tells the developer directly to generate a new key rather than retry with the old one.
That last point sounds small. It ends up being the difference between documentation that describes behavior and documentation that actually helps someone build the thing correctly on the first try, instead of the third.
Adyen's documentation takes a similar approach from a different angle. It specifies the 409 or 422 response with error code 704 for concurrent duplicate requests, and it recommends exponential backoff explicitly, as part of the documented contract rather than an implementation detail buried somewhere in a changelog. The error code itself is treated as public API surface, something client code branches on reliably, rather than an internal detail that might shift between releases without warning anyone.
What both companies share is treating idempotency as a first-class part of the contract, not an aside tacked on at the bottom of a page. The behavior under retry, the exact key format, the expiration window, the full set of error responses: all of it gets specified rather than implied.
What's usually missing in less mature API docs is close to the mirror image of what Stripe and Adyen get right. Does idempotency cover error responses, or only successes? What happens to a request that fails validation before execution even starts? What's the actual expiration window, because without one a developer literally cannot write correct retry logic no matter how careful they are? And in a multi-tenant system, how are keys scoped? Leave any of these out, and you've left a design decision as an implicit assumption that some developer, somewhere, under production load, is going to guess wrong about.
What complete idempotency documentation looks like in an API reference
Full idempotency documentation lives in at least two places, and skipping either one leaves a gap. There should be a dedicated conceptual page explaining the mechanism end to end, and there should be inline documentation on every individual endpoint where the behavior actually applies.
The conceptual page needs to cover which methods are natively idempotent, and walk through the effect-versus-response distinction clearly enough that a reader who's never encountered it before actually gets it on the first pass. It needs to explain how idempotency keys work for POST and PATCH specifically: how to generate one, what format is expected, where it goes in the request. It needs to state the replay window and describe what happens once a key expires. It needs to call out the validation-before-execution nuance explicitly, because that's the detail that trips up even experienced engineers who've built retry logic before, on other systems, more than once. And it needs to tell the reader what to do after a permanent error: generate a new key, don't retry the old one.
Per-endpoint documentation is where the general rules meet specific reality. Each endpoint should state whether it accepts an idempotency key at all, and if so, whether that key is required, optional, or simply not applicable because the method is already idempotent by nature. It should list which response codes are possible on a replay, including the 409 case for concurrent duplicates. And it should flag any scoping or expiration behavior that deviates from the global default, because unmentioned exceptions are exactly the kind of thing that surfaces mid-incident, not during a calm afternoon of reading docs.
Retry guidance deserves a home in the docs themselves, rather than staying buried inside an SDK's source where only the most persistent developers ever find it. Someone hitting the raw API directly, without an official client library, needs the recommended backoff strategy just as much as someone using a generated SDK does.
The common omissions are worth naming directly, since they keep recurring across the industry: the key length limit, the explicit warning against sensitive data in keys, and the distinction between a request that failed validation and one that failed after execution actually began. Each is small on its own. Skip a few together, though, and you've built documentation that reads as complete while leaving gaps a developer only discovers the hard way, usually during an incident review.
Why documentation that drifts from implementation makes idempotency guarantees meaningless
Idempotency is a runtime contract between an API and everyone building against it. If the documented behavior diverges from what the server actually does, every client built against that documentation is writing retry logic against a description of a system that no longer exists. That's a correctness bug wearing documentation's clothing, and it's harder to spot than a broken test.
Which behaviors drift most, and cause the most damage when they do? Expiration windows top the list. Stripe's move from a 24-hour window in v1 to 30 days in v2 is a real, concrete case, and any client that hardcoded assumptions from the old window needed to relearn the new one to keep working correctly. Which methods accept idempotency keys is another: v1 covers POST only, v2 extends coverage to DELETE, and that's a meaningful behavioral difference for anyone building against either version. Error codes for concurrent requests or payload mismatches round out the list, the kind of detail that changes quietly and breaks retry logic just as quietly, weeks later, for reasons nobody traces back to a docs update.
Here's a rough test worth running against your own team. If updating the idempotency contract in your public documentation is a hassle, something that needs a separate ticket, a different team, a delayed publish cycle, that friction is itself a signal. It means the documentation lives apart from the codebase instead of inside the actual development workflow. Things that live apart from the workflow drift, slowly at first and then all at once, until nobody's quite sure which version of the docs matches which version of the API.
The stakes here have gone up recently, worth saying plainly. AI-assisted workflows and autonomous agents that consume APIs directly are becoming a common integration pattern rather than a hypothetical one. An agent parsing your documentation to generate its own retry logic will faithfully implement whatever behavior the docs describe, right or wrong, with no human in the loop to catch a stale page before the code ships. When that error propagates, it doesn't scale with one careless developer's mistake anymore. It scales with every agent-driven integration built against the same stale page, simultaneously.
Documentation and API design turn out to be the same problem, just viewed from two different angles. Knowledge infrastructure that stays synced with the actual implementation, where docs function as a living part of the codebase rather than a PDF someone updates once a quarter if they remember, is what makes an idempotency contract durable enough to trust over years rather than months. Mintlify is one tool built around that idea, treating documentation as something that stays wired into the same workflows and tools consuming it, rather than a separate artifact that gets updated on its own schedule and quietly falls behind.
An idempotency guarantee is only as good as the accuracy of whatever describes it to the outside world. The documentation problem was never really separate from the design problem. It's the same question, just asked by a different desk depending on the week.


