go2llm API documentation

409 idempotency_conflict: an Idempotency-Key reused for a different request

A 409 idempotency_conflict on go2llm means the Idempotency-Key header was already used for a request with a different body. go2llm checks this on exactly one route, POST /v1/images/generations — idempotency is mandatory there because image generation spends real money and shouldn't duplicate just because the same request got sent twice. A second, separate cause shares the same 409 status: request_in_progress, when the first request under that key hasn't finished yet.

Written 2026-09-22.

What the error looks like

go2llm answers with status 409 and the standard error envelope — error.type and error.code are both idempotency_conflict, error.message explains the reason in words, and error.request_id starts with gw_. The exact wording of message for a conflict hasn't been captured from a live request for this page — reproducing it needs a working go2llm key (two requests with the same Idempotency-Key and different bodies), and without a key, authentication is checked before idempotency: any request to this route with no key gets 401 no-key, never reaching the conflict check at all.

The second scenario under the same status is the one place a retry-after header shows up: if a request under this Idempotency-Key is still running, go2llm answers 409 request_in_progress with Retry-After in seconds, rather than kicking off the generation a second time. idempotency_conflict carries no Retry-After — resending the same key with a different body won't become valid no matter how long you wait.

What causes it

The same key sent with a different request body

The classic case: code updates the prompt after a failed or canceled attempt but keeps reusing the old Idempotency-Key variable. go2llm sees the same key attached to a different body and refuses the request instead of quietly running the new one under the old key.

A task ID used directly as the Idempotency-Key, with no version

If the key comes straight from an order ID or a task ID with no attempt number attached (order-42 instead of order-42-attempt-2), any change to the generation parameters for that same order — a different size, a different prompt, a different model — produces idempotency_conflict, even though it's a legitimate new image for the same order.

A resend went out before the first request finished

A client with a short timeout, or an aggressive retry policy, can send a repeat under the same Idempotency-Key while the first request is still being processed upstream. That's not idempotency_conflict — it's request_in_progress, and it isn't a client mistake so much as generation that simply hasn't finished yet.

The key doesn't match the format — a different error, not 409

go2llm requires 8–128 printable ASCII characters for Idempotency-Key. A key that's shorter, longer, or carries non-printable or non-ASCII characters — curly quotes pasted in from a clipboard by accident, say — gets not a 409 but a 400 idempotency_key_required, confirmed against a live request:

{
  "error": {
    "type": "idempotency_key_required",
    "code": "idempotency_key_required",
    "message": "Idempotency-Key must be 8-128 printable ASCII characters",
    "request_id": "gw_e8287de12a754324b9792feedaf6007c"
  }
}

That's a separate format check, and it runs first: the server confirms the header is valid at all before it ever compares the body against whatever's already stored under that key.

Fixing it step by step

Step 1. Check the key's format before sending it:

def valid_idempotency_key(key: str) -> bool:
    return 8 <= len(key) <= 128 and key.isascii() and key.isprintable()

Step 2. Build the key from what you're generating plus the attempt, not from the task ID alone:

import hashlib

def make_idempotency_key(order_id: str, attempt: int, prompt: str) -> str:
    payload_hash = hashlib.sha256(prompt.encode()).hexdigest()[:16]
    return f"{order_id}-a{attempt}-{payload_hash}"[:128]

Hashing the prompt into the key isn't something go2llm requires — it's a practical trick. If the request body genuinely hasn't changed, the key stays the same on its own, and a repeat under it is a safe retry rather than a conflict. Change the content and the key changes with it, so no conflict ever arises.

Step 3. Tell the two 409 codes apart in your handler — they call for different responses:

import time
import httpx

def generate_image(payload: dict, idempotency_key: str, api_key: str):
    with httpx.Client(timeout=60.0) as client:
        r = client.post(
            "https://go2llm.tech/v1/images/generations",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Idempotency-Key": idempotency_key,
                "content-type": "application/json",
            },
            json=payload,
        )
        if r.status_code == 409:
            err = r.json().get("error", {})
            if err.get("code") == "request_in_progress":
                delay = float(r.headers.get("retry-after", 2))
                time.sleep(delay)
                return generate_image(payload, idempotency_key, api_key)
            raise ValueError(
                f"Idempotency-Key {idempotency_key!r} is already tied to a different body — "
                f"use a new key, don't resend the same one"
            )
        r.raise_for_status()
        return r.json()

Step 4. Confirm it with a live request on a minimal example:

curl https://go2llm.tech/v1/images/generations \
  -H "Authorization: Bearer $GO2LLM_API_KEY" \
  -H "Idempotency-Key: test-run-$(date +%s)" \
  -H "content-type: application/json" \
  -d '{"model":"gpt-image-2","prompt":"A minimal diagram of an API gateway","n":1,"size":"1024x1024","quality":"standard","response_format":"url"}'

The unique key built from $(date +%s) guarantees this particular test never collides with a key already used in an earlier check.

Why Idempotency-Key exists at all

A unique key header for idempotency is a common pattern for any API where a repeat request costs money. If the connection drops after the server accepted a request but before the client saw the response, the client has no way to tell "the request never arrived" apart from "the request ran and the response got lost." Without idempotency, a retry that looks perfectly safe silently creates a second generation and a second charge. Idempotency-Key fixes this at the protocol level: the server remembers which key went with which body, and refuses instead of running the operation twice.

go2llm applies this to exactly one route, POST /v1/images/generations — the one place where a single call produces one billable artifact all at once, rather than a token stream that can be cut off partway through and simply not billed for what never arrived.

Related errors

FAQ

Does /v1/messages or /v1/chat/completions need an Idempotency-Key? No, go2llm only requires it on POST /v1/images/generations. The text endpoints don't check header-level idempotency at all.

Can the same Idempotency-Key be reused for two requests that really are identical? Yes — that's the normal case. If the body is identical, a repeat under the same key is a safe retry, not an error. 409 idempotency_conflict only fires on a different body under the same key.

Does a request that returns 409 get charged? No, in either case. idempotency_conflict stops the request before generation runs. request_in_progress is just the answer to an already-sent first request — it doesn't create a second charge.

How long should I wait after a request_in_progress? Exactly what the Retry-After header says — go2llm sets it specifically for this case. Don't hardcode a fixed pause: actual generation time varies by model.

Why does a malformed key get 400 instead of 409? 409 means a conflict with something that already exists, and if the key is invalid by format, there's nothing yet to compare it against — the server rejects the header at the validation stage, before it even looks for a matching key. Tell the two apart by error.code, not the HTTP status: idempotency_key_required is fixed by changing the key's format, idempotency_conflict by switching to a new key.


Get a key and make your first request in 3 minutes → go2llm.tech. Per-model prices are on the pricing page, setup steps are in the docs.