Retry safely with Idempotency-Key
You send a request that costs money, and the connection drops. Did Hyperouter get it? You cannot tell. If you send it again and it had arrived, you start a second job and pay twice.
An Idempotency-Key header removes the doubt. Hyperouter recognises the repeat, starts no second job, places no second hold, and answers from the first request.
Send one on every request that costs money
Generate a new random value, such as a UUID v4, for each new request. Keep it with the request, and send the same value every time you send that request again.
curl https://api.hyperouter.app/v1/images/generations \
-H "Authorization: Bearer $HYPEROUTER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-d '{"model": "x-ai/grok-imagine-image", "prompt": "A lighthouse at dusk", "n": 1}'
The OpenAI SDKs do not send this header on their own. Pass it as an extra header, and the SDKs re-send it unchanged on their automatic retries.
import uuid key = str(uuid.uuid4()) # one per request; reuse it when you retry this request img = client.images.generate( model="x-ai/grok-imagine-image", prompt="A lighthouse at dusk", extra_headers={"Idempotency-Key": key}, )
import { randomUUID } from "node:crypto"; const key = randomUUID(); // one per request; reuse it when you retry this request const img = await client.images.generate( { model: "x-ai/grok-imagine-image", prompt: "A lighthouse at dusk" }, { headers: { "Idempotency-Key": key } }, );
What each endpoint replays
| Endpoint | A repeat returns |
|---|---|
POST /v1/images/generations, POST /v1/images/edits | The first request's image, or its 202 with the same id under Prefer: respond-async. |
POST /v1/videos, /v1/videos/edits, /v1/videos/extensions, /v1/videos/{id}/remix | The first request's Video object, as it is now. |
POST /v1/chat/completions | 409 idempotency_key_replayed with the first request's id. |
Chat is different
Chat replies are not replayed. A repeat with the same key answers 409 with code idempotency_key_replayed, and error.generation_id is the first request's id. The request is not run or billed again.
{
"error": {
"message": "A request with this Idempotency-Key already ran as generation 019a1f3c-5b7e-7d21-9c4a-2e8f6b0d3a17. ...",
"type": "conflict",
"code": "idempotency_key_replayed",
"param": null,
"generation_id": "019a1f3c-5b7e-7d21-9c4a-2e8f6b0d3a17"
}
}
Look the first request up with GET /v1/generations/{id} to see its status, usage and cost. If the first chat completion failed and was not billed, its key is freed, and a repeat runs as a new request.
So for chat, the key is a guard against a double charge, not a way to fetch the answer again. Treat the 409 as "it already ran", and read the record.
import os
import httpx
import openai
try:
res = client.chat.completions.create(
model="x-ai/grok-4.7",
messages=[{"role": "user", "content": "Hello"}],
extra_headers={"Idempotency-Key": key},
)
except openai.ConflictError as e:
body = e.body or {} # the error object
if body.get("code") != "idempotency_key_replayed":
raise
record = httpx.get(
f"https://api.hyperouter.app/v1/generations/{body['generation_id']}",
headers={"Authorization": f"Bearer {os.environ['HYPEROUTER_API_KEY']}"},
).json()["data"]
Match on code, never on message.
What a repeat gets
| You send | Hyperouter answers |
|---|---|
| A new key | Runs the request as usual. |
| The same key and the same request, after the first was accepted | The first request's result, as in the table above. No second job, no second hold, no second charge. |
| The same key and the same request, while the first is still being accepted | Waits for the first, then answers as above. If the first has not started its job within 60 seconds: 409 idempotency_key_in_use. Retry with the same key. |
| The same key and a different request | 409 idempotency_key_reused. Nothing runs. Use a new key. |
The same key, after the first was refused before any job started (a 400, a 402, or a 5xx sent without x-should-retry: false) | Runs the request as new, so you can correct it and send it again with the same key. |
| The same key, more than 30 minutes after the first request | Runs the request as new. |
| A key longer than 255 characters, or not printable ASCII | 400 invalid_idempotency_key. Nothing runs. |
Rules
- Scope. A key belongs to the API key that sent it. Two API keys can use the same value without affecting each other.
- Window. A key is honoured for 30 minutes after the first request. Retry within that time.
- Same request. Two requests are the same when the method, the path and the body match. JSON is compared after parsing, so field order and whitespace do not matter. For a multipart upload, the file bytes are compared too.
- Billing. A request made under a key is billed at most once, whatever the number of repeats. A repeat answered from the first request costs nothing.
- Guarantee. One key never starts more than one job, including when two repeats arrive at the same time.
Automatic retries in the SDKs
The OpenAI SDKs retry a 408, 409, 429 or 5xx answer twice by default, unless the answer carries the response header x-should-retry: false. Hyperouter sends that header on every answer where a retry would start a second job or a second charge: the 503 timeout of a sync image, a 500 after the job was created, and 409 idempotency_key_reused and idempotency_key_replayed.
It never sends it on a 429, on 409 idempotency_key_in_use, or on a 5xx from before any job exists. Those are safe to retry. With an Idempotency-Key, every retry is safe.
The full rules are in Retries and idempotency.