422 idempotency_conflict: what it means and how to fix it
You retried a request with the same idempotency key but a different body, and got a 422 instead of the expected replayed response. Here is why.
4 min · · Updated · errors
Retry logic built on idempotency keys usually works quietly and invisibly — right up until the server starts returning 422 on requests that look like ordinary retries. Here is what this conflict is and where it comes from.
What Idempotency-Key does
The Idempotency-Key header protects against double charging when the same request gets sent twice — for example, if a client times out waiting for a response and decides to retry. If a request arrives within 24 hours carrying the same key value, the server returns the same result as the first time, without re-executing the request or charging again.
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer fetch_your_key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-H "Idempotency-Key: req-2026-09-16-a1b2c3" \
-d '{"model":"claude-sonnet-5","max_tokens":256,"messages":[{"role":"user","content":"Hello"}]}'Repeat this exact request with the same Idempotency-Key and the same body, and you get the same response instantly, with no second charge.
Where the 422 comes from
{"error": {"message": "Idempotency key already used with a different request body", "code": "idempotency_conflict"}}The conflict happens when a request with a different body arrives under the same Idempotency-Key value. The whole mechanism assumes a key uniquely describes one particular request — if the body changed but the key stayed the same, the server can't tell which of the two is correct, so it honestly refuses instead of returning a stale cached answer or silently overwriting history.
Common cause: generating the key without the content
The most typical mistake is deriving the idempotency key from something unrelated to the request's content — a user session ID, or the current minute:
# Bad: the same key for every request within a session
idempotency_key = f"session-{session_id}"
# Every new question the user asks in that session gets the same key,
# and the second request with different content fails with 422A key set in the client's default headers (for example, default_headers when constructing an OpenAI SDK client) has the same effect: it goes out with every single request. Set Idempotency-Key on the specific call (extra_headers in .create()), unique per logical request.
The right approach is to base the key on a hash of the request content, not just an external identifier:
import hashlib
import json
def make_idempotency_key(payload):
body_hash = hashlib.sha256(
json.dumps(payload, sort_keys=True).encode()
).hexdigest()[:16]
return f"req-{body_hash}"
payload = {
"model": "claude-sonnet-5",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Hello"}],
}
key = make_idempotency_key(payload)This way the key changes automatically with the content: a genuine retry of the same request matches by key, and a new request does not.
Another common cause: the body drifts between retries
Sometimes the key is generated correctly, but between the first attempt and the retry something variable sneaks into the meaningful part of the body — a timestamp inside the message text, a recomputed max_tokens, a reordered message list:
# Risky: the timestamp changes on every call, even with the same Idempotency-Key
payload = {
"model": "claude-sonnet-5",
"messages": [
{"role": "user", "content": f"[{datetime.utcnow().isoformat()}] {text}"} # drifts
],
}The gateway ignores the metadata, user and stream_options fields when comparing, so those can change between attempts without a conflict. Everything in messages, system, temperature, max_tokens and the other generation parameters must stay identical across retries: keep service timestamps out of those fields, and generate the idempotency key from the meaningful part of the request (model, messages, generation parameters).
How to fix it in practice
Three concrete steps:
Check your key generator. The key should depend on request content that could genuinely change, and should not depend on things that vary between calls that are semantically identical — timestamps, random logging IDs.
Log the key-body pair before sending. If the 422 reproduces, diff the first and second request bodies line by line — usually the difference is a single field that slipped in by accident.
Don't reuse keys manually while testing. If you're poking the API by hand with curl and change the -d payload, remember to change Idempotency-Key too — otherwise the first attempt at "just retrying with a different prompt" will hit this exact conflict.
Idempotency works the same way across modes
Idempotency-Key is a general gateway mechanism, not tied to a specific path. The rule is identical for /v1/messages (Anthropic-style), /v1/chat/completions and /v1/responses (OpenAI-style); balance top-ups aren't covered by the mechanism: the same key must accompany the same body, or you get a 422, regardless of which protocol you're using.
An example with automatic retry
The idempotency key earns its keep specifically when paired with timeout-driven retries:
import time
import requests
def send_with_retry(payload, api_key, key, max_attempts=3):
for attempt in range(max_attempts):
try:
resp = requests.post(
"https://api.zukko.pro/v1/messages",
headers={
"Authorization": f"Bearer {api_key}",
"anthropic-version": "2023-06-01",
"Idempotency-Key": key, # same value across all attempts
},
json=payload,
timeout=60,
)
resp.raise_for_status()
return resp.json()
except requests.exceptions.Timeout:
if attempt == max_attempts - 1:
raise
time.sleep(2 ** attempt)The idempotency key here is deliberately the same across every attempt of one logical request — that's exactly how the mechanism is meant to be used, so a retry caused by a timeout doesn't lead to a double charge. A 422 conflict in this scenario would only happen if, between attempts, something else — say, a parallel worker with the same key-generation logic — sent a different body under the same value.
What is not a fix
Don't just catch the 422 and generate a fresh random Idempotency-Key on every error — that defeats the entire point of the header, which exists to prevent double charging. If the request is genuinely new in meaning, it should get a new key derived from its own content. If it's genuinely the same request, the real problem is that the body is drifting somewhere in the pipeline, and that's worth finding rather than working around.
Bottom line
422 idempotency_conflict is not a server failure — it's a signal that the same Idempotency-Key is being used for two requests with different content. The fix lives on the client side: the key should deterministically depend on the meaningful part of the request body, and should not depend on variable fields like timestamps.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- OpenAI API Keys in the Open: Why You Should Not Use ThemWhere free keys on GitHub come from, why using them is dangerous for you and for the owner, and how to protect your own key from the same leak.
- GLM API Errors: Why the Request Fails and How to Fix ItA wrong model id, an overflowing 128K context, the key, the balance, limits: the typical causes of GLM API errors and a quick way to find yours.
- This organization has been disabled in the Claude API: What to DoAn Anthropic key suddenly answers "organization has been disabled". What the error is, what to check, and how to move the SDK and Claude Code to another provider.
- JetBrains AI Assistant not working: causes and workaroundsAI Assistant will not activate, stays silent or is off for some files. We go through the causes in the JetBrains docs and connect your own models via a gateway.