Idempotency-Key: retrying requests without paying twice

A client-side timeout doesn't mean the request never ran on the server. The Idempotency-Key header solves that without manually checking usage history.

4 min · · Updated · API · reliability

The connection drops mid-request, an HTTP client times out before the model finishes answering — and on the server, the request may have completed and been charged in full. Without protection against retries, the only safe option is to check /v1/usage manually before retrying by hand. Idempotency-Key solves this at the protocol level.

How it works

Attach an Idempotency-Key header with an arbitrary unique string to your request. If another request with the same header value and the same body arrives within 24 hours, the server returns the exact same response without re-running the request or charging twice. This works for POST /v1/chat/completions, /v1/messages and /v1/responses — not for balance top-ups.

bash
curl https://api.zukko.pro/v1/chat/completions \
  -H "Authorization: Bearer fetch_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-48213-email-draft" \
  -d '{
    "model": "gpt-5.6-sol",
    "messages": [{"role": "user", "content": "Draft a customer email"}]
  }'

Repeat this exact curl with the same Idempotency-Key and the same JSON body, and the response comes from the already-completed request — no second call to the model, no second charge.

When you actually need it

A connection cut mid-response. As covered in the errors reference: a mid-response cutoff is almost always a client or network timeout, not a token limit. Even when the connection drops, the gateway reads the provider's response to the end and bills the full generation, while the client never found out. A retry with the same Idempotency-Key will not create a second charge.

Automatic retries in task queues. If a worker crashes after sending a request but before recording the result, a restart will replay the call. Without idempotency that's a double charge for one generation — especially noticeable on requests with a large context.

Flaky networks on mobile clients. A bad connection is a common reason for a user tapping "send" twice. Idempotency-Key on the backend protects against both taps reaching the model as two separate requests.

How to pick the key value

The key needs to be unique per logical request and identical across all of that request's retries. Set it on the specific call, not in the client's default headers (for example, default_headers in the OpenAI SDK) — otherwise the next, different request gets a 422 idempotency_conflict. The simplest approach is generating a UUID once before the first attempt and reusing it across retries of that same request:

bash
import { randomUUID } from "crypto";

async function sendWithRetry(body, maxAttempts = 3) {
  const idempotencyKey = randomUUID();

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const resp = await fetch("https://api.zukko.pro/v1/chat/completions", {
        method: "POST",
        headers: {
          "Authorization": "Bearer fetch_your_key",
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify(body),
      });
      if (resp.ok) return resp.json();
    } catch (e) {
      if (attempt === maxAttempts) throw e;
    }
  }
}

Important detail: idempotencyKey is created once before the retry loop, not inside it — otherwise every attempt becomes a brand-new request again.

The 422 idempotency_conflict error

Send the same Idempotency-Key with a different body, and the server answers 422 with code idempotency_conflict (the metadata, user and stream_options fields don't count toward the comparison):

bash
{
  "error": {
    "message": "Idempotency-Key already used with a different request body",
    "code": "idempotency_conflict"
  }
}

This guards against the opposite bug: if your code accidentally reuses one key for two different requests (say, a counter collision instead of a UUID), the server won't silently return an unrelated response — it refuses explicitly.

The practical takeaway: don't build Idempotency-Key values from an incremental counter like request-1, request-2 within a session — if the counter ever resets, you'll collide with someone else's request body and get a 422 instead of a normal answer.

What Idempotency-Key does not do

It does not replace response caching and doesn't save money on requests that differ in content — it is literally protection against paying twice for the same request on retry. If you need to avoid paying for repeated-but-separately-timed identical prompts, that's the separate caching feature triggered by the X-Zukko-Cache: true header (or temperature: 0 on models that accept it, not Claude).

It also doesn't extend a response's lifetime indefinitely — the deduplication guarantee holds for 24 hours. Retry a day after the first attempt, and it's a new request with its own charge.

Bottom line

Idempotency-Key is one header that removes an entire class of "did the request actually go through, and I just never got the answer" problems. Generate a key once per logical request, reuse it across retries of the same body, and never reuse it across requests with different content — or you'll get a 422 instead of a result.

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.

Get API keyDocumentation

api.zukko.pro · fetch_*

Idempotency-Key header: safe request retries · Zukko