Retry safely with Idempotency-Key

· Hyperouter · 5 min read

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},
)

What each endpoint replays

EndpointA repeat returns
POST /v1/images/generations, POST /v1/images/editsThe 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}/remixThe first request's Video object, as it is now.
POST /v1/chat/completions409 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 sendHyperouter answers
A new keyRuns the request as usual.
The same key and the same request, after the first was acceptedThe 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 acceptedWaits 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 request409 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 requestRuns the request as new.
A key longer than 255 characters, or not printable ASCII400 invalid_idempotency_key. Nothing runs.

Rules

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.