A client reuses the same Idempotency-Key HTTP header value but sends a different request body than the first time. How should the API respond, and how does the server detect this?
answer
- Fingerprint = hash(method + path + raw body bytes)
- Mismatch → reject, execute nothing (Stripe uses 422)
- Never replay the old response for a different payload
- Hash received bytes, not re-serialized JSON
- Different auth principal = different scope, not a mismatch
basics
~20 sReject it without executing anything. The server stores a fingerprint (hash of method, path and body) with each key and compares on every arrival; a mismatch means the client has a bug, so return an error — commonly 422 Unprocessable Content — rather than replaying or executing.
solid answer
~50 sThe server stores a **request fingerprint** — typically a SHA-256 over the method, request target and the raw body bytes — alongside each key. On every request carrying a key it recomputes the fingerprint and compares. If it matches, the request is a genuine retry: replay the stored response. If it **differs**, the client is reusing a key for a different operation, which is always a client-side bug. The server must **execute nothing** and return an error; Stripe returns **422 Unprocessable Content**, and 400 or 409 Conflict are also used. The error body should say plainly that the key was already used with a different payload. This check exists because without it the two failure modes are both bad: replaying the first response for a different request silently discards the second operation, and executing it would let one key produce two different effects. Hash the **raw bytes** received, not a re-serialized object — JSON key ordering and whitespace differ between serializers, and canonicalizing invites subtle false mismatches.
code
http · 21 linesPOST /v1/payments HTTP/1.1
Idempotency-Key: 8f14e45f-ceea-467a-9f5a-3c3f2f0d1b77
Content-Type: application/json
{"amount":2000,"currency":"usd"}
HTTP/1.1 201 Created
--- later, same key, different body ---
POST /v1/payments HTTP/1.1
Idempotency-Key: 8f14e45f-ceea-467a-9f5a-3c3f2f0d1b77
Content-Type: application/json
{"amount":5000,"currency":"usd"}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{"error":{"code":"idempotency_key_reused",
"message":"This Idempotency-Key was already used with a different request body."}}go deeper
Say the server keeps a hash of the original request and rejects a reused key whose body differs, without executing anything.
Name the fingerprint contents, the status code choice (422 is the common convention), and why replaying would silently swallow the second operation.
Discuss hashing raw bytes versus canonical JSON, what belongs in the fingerprint versus the key's scope, distinguishing this error from the in-progress 409, and monitoring mismatch rates as a client-bug signal.
Define it as a platform-wide contract: one error code across services, SDK behavior that never retries it, and the observability that catches a client shipping a key-reuse bug before customers report it.
## Why the check exists An idempotency key is a client's assertion: "this is the same logical operation I told you about before." If the payload has changed, that assertion is false. Something in the client is wrong — a key reused across a loop iteration, a key derived from a non-unique value like a user id, a key cached longer than the operation it named. Without a fingerprint check the server has two choices, both unacceptable: - **Replay the first response.** The client sends "charge $50", receives the stored 201 for a $20 charge, and believes $50 was charged. The second operation vanishes silently. This is the worse outcome, because it looks like success. - **Execute the new request under the old key.** Now one key maps to two different effects and the dedupe record is meaningless. So the server rejects, loudly and without side effects. ## Computing the fingerprint A typical fingerprint is `SHA-256(method || request-target || body-bytes)`, stored as a column on the dedupe record. **Hash the raw received bytes.** It is tempting to parse the JSON and hash a canonical form, but canonicalization is a swamp: key ordering, whitespace, number formatting (`1.0` vs `1`), Unicode normalization, and optional fields with default values all vary between client libraries and language runtimes. Hashing exactly what arrived means two byte-identical retries always match — which is precisely the case that must work — at the cost of flagging a client whose serializer reorders keys between attempts. That is an acceptable trade because a correct retry re-sends the buffered original bytes; a client that re-serializes from an object on each attempt is already fragile. **Include method and path.** Otherwise the same key on `POST /payments` and `POST /refunds` with structurally similar bodies could collide. Many APIs also make the endpoint part of the key's *scope*, which achieves the same separation. **Exclude volatile headers.** Do not fold `Date`, `User-Agent`, tracing headers or auth tokens into the fingerprint — they legitimately change between retries. Authentication identity should instead be part of the key's **scope**: a key presented by a different account is not a mismatch, it is a different namespace entirely, and treating it as a mismatch would leak the existence of another tenant's key. ## Choosing the status code There is no standard, so document whatever you choose. - **422 Unprocessable Content** — Stripe's choice. The request is syntactically fine but semantically unprocessable because the key is bound to a different payload. Widely copied. - **400 Bad Request** — defensible, treats it as a malformed request; less specific. - **409 Conflict** — reads well ("conflicts with existing state"), but many implementations already use 409 for *in-progress* keys, and using it for both makes the two situations indistinguishable to the client, which matters because one is retryable and the other is not. Whatever the code, the response body should carry a machine-readable error code (e.g. `idempotency_key_reused`) and a human-readable message naming the problem, because the fix is always in the client. ## What the client should do about it Nothing retry-shaped. A mismatch is not transient: retrying the same request with the same key produces the same rejection forever. The correct client response is to **generate a new key** for the new operation, and to fix whatever caused the reuse. This distinction matters for SDK authors — the mismatch error must not be classified as retryable. ## Adjacent decisions **Should the check run before or after authorization?** After. The key lookup is scoped to the authenticated principal, so you need the identity first, and you should not reveal anything about stored keys to an unauthenticated caller. **What about a mismatch against an `in_progress` record?** Same rejection. The first operation is still running under this key; a different payload cannot be admitted regardless. **Do you store the offending request?** Not against the key, but logging the fingerprint mismatch with both hashes is valuable operationally — a spike in mismatches usually means a client deployed a bug, and you want to notice before their support ticket. ## Summary line for an interview "I store a hash of the method, path and raw body with the key. Matching hash means retry, so I replay the stored response; differing hash means the client reused a key for a different operation, so I execute nothing and return 422 with a specific error code. And I hash the received bytes rather than a canonicalized form, because canonicalization creates false mismatches."
- Why not simply replay the stored response when the payload differs?Because the client would believe the new operation succeeded when it never ran. If the first request charged $20 and the second asks for $50, replaying returns a successful $20 result and the $50 charge silently disappears — a failure that looks like success, which is the worst kind.
- Should the fingerprint be computed over canonicalized JSON?Better to hash the raw bytes as received. Canonicalization has to settle key ordering, whitespace, number formatting and Unicode normalization, and any disagreement between your rules and the client's serializer produces false mismatches. A correct retry re-sends the buffered original bytes, so byte hashing matches exactly when it should.
- Is a key-mismatch error retryable by the client?No. It is deterministic — the same key with the same differing payload will be rejected forever. The client must generate a fresh key for the new operation and fix the reuse bug, so SDKs must classify this error as non-retryable.
saying these in an interview costs you the question
- Replaying the stored response when the body differs.
- Executing the new request anyway because 'the key already succeeded, so this must be new'.
- Including volatile headers like Date, User-Agent or trace ids in the fingerprint.
- Treating a key presented by a different account as a payload mismatch rather than a separate scope.
- Classifying the mismatch error as retryable in the client SDK.