POST /v1/runs requires an Idempotency-Key header, so a retry of the same request returns the stored run instead of running and charging it twice. Read this page before you write retry logic, queue workers or anything else that can send the same run more than once.
The header
A missing header is refused with
400 idempotency_key_required. An empty or overlong value, or one that is not valid header text, is refused with 400 invalid_idempotency_key.
What makes two requests “the same”
OpenType stores a digest of each run’s input next to its key. Two requests with the same key count as the same request when these parts match:
These fields are not part of the identity:
max_output_tokens, deadline_ms and capability_hint. A second request that changes only one of them, with the same key, replays the first run. It does not run again with the new setting. Use a new key when you change them on purpose.
What you get back
- A replay is never charged again. It makes no model call.
- A replay skips the quota and credit checks. A key that already owns a run is never refused with
402or429. - A replay is thinner than the live response. It has the same shape as
GET /v1/runs/{run_id}: nocost_basis, noschema_enforcement, and a decision withoutmodel,stagesor the thought fields. - A failed run replays as
200. Checkstate, not only the HTTP status.
202 because the stored run is still pending:
202 has two causes. Either the first request is still being served, or the run failed before a model call and will stay pending for good. Polling explains how to tell them apart.
Choose a key per logical request
Derive the key from your own business id and the question you ask, not from the HTTP attempt:- Good:
ticket-4822-triage. The same ticket and the same question always give the same key, so a worker that crashes and restarts sends the same key again and gets the stored run. - Good: a UUID that you generate once, store with the job, and send on every attempt of that job.
- Bad: a fresh UUID per HTTP call. Every retry becomes a new run and a new charge.
- Prefix keys per service, for example
support-bot:ticket-4822-triage, so two services in one organization cannot collide. - Version the key when the request changes on purpose. If you edit the question set for ticket 4822, send
ticket-4822-triage-v2. The old key still belongs to the old body and answers409to the new one. - Keep keys well under 255 bytes if your retry logic appends a suffix, as the helper in Error handling does.
Reuse the key or mint a new one
The rule depends on whether the failed request left a run behind.
A
5xx may come after the run was stored. A run that failed after a model call settles as failed and replays as 200 with "state": "failed". A run that failed before a model call stays pending and replays as 202 indefinitely. Neither runs again under the same key, so a new key is the only way to try again.
Example
This sends a triage run keyed on the ticket id and handles each outcome.What goes wrong
Related
- Error handling - which statuses to retry, with a helper that moves to a new key after a
5xx. - Polling - recover a run after a client timeout and tell a slow run from a stuck one.
- idempotency_conflict - the same key sent with a different body.
- Runs - the run lifecycle and what a run returns.
- API reference - the full
POST /v1/runscontract.