Idempotency
Keel documents idempotency separately for the canonical permit flow and the execution routes.
At a glance
| Route family | Where idempotency goes |
|---|---|
POST /v1/permits | JSON body idempotency_key |
POST /v1/permits/{permit_id}/usage | JSON body usage_idempotency_key |
POST /v1/executions | Idempotency-Key header |
POST /v1/execute | Idempotency-Key header |
POST /v1/proxy/* | Idempotency-Key header |
POST /v1/mcp/servers/{server_ref}/tools/{tool_name}:call | JSON body idempotency_key or Idempotency-Key header |
Permit idempotency
Route:
POST /v1/permits
Canonical field:
idempotency_keyin the JSON body
Behavior:
- scope is per project
- same key plus the same semantic payload replays the prior permit response
- same key plus a different semantic payload returns
409 conflict - if the client omits
idempotency_key, Keel generates a server-side idempotency key for that permit - replay is still route-dependent: stale
allowpermits with inactive reservations can expire or revalidate instead of replaying the originalallowbody
Permit usage closeout idempotency
Route:
POST /v1/permits/{permit_id}/usage
Canonical field:
usage_idempotency_keyin the JSON body
Behavior:
- use the same key when retrying the same permit closeout
- same key plus the same closeout values replays the existing closeout response
- same key plus different closeout values returns
409 conflict - this is a narrower permit-first closeout contract, not proxy-strength execution replay binding
Execution idempotency
Routes:
POST /v1/executionsPOST /v1/executePOST /v1/proxy/openaiPOST /v1/proxy/anthropicPOST /v1/proxy/googlePOST /v1/proxy/xaiPOST /v1/proxy/meta
Canonical field:
Idempotency-KeyHTTP header
Behavior:
- reuse the same header value when retrying the same execution request
- if the header is omitted, the request still runs, but retries are treated as new executions
- proxy routes bind replay to the same key and payload; the same key plus a different payload returns
409 conflict - a proxy stream replay requires complete original SSE bytes in the bounded response store; unavailable or incomplete stream bytes return
409without another provider dispatch, rather than an empty successful stream - historical normalized-only stream caches cannot reconstruct the original SSE and are refused with
409. Do not switch to a new idempotency key to force a redispatch; reconcile the original outcome first. - execution routes use the same header-based retry pattern, but you should still treat the header as part of the public route contract rather than assume identical semantics across every route family
/v1/executionsand/v1/executealso bind replay to a hash of the request; the same key plus different request content returns409with an execution envelope (error.code: "idempotency_conflict"); while the first request with that key is still running, a retry returns503idempotency_in_progresswithRetry-After— retry with the same key
MCP tool-call idempotency
Route:
POST /v1/mcp/servers/{server_ref}/tools/{tool_name}:call
Canonical fields:
idempotency_keyin the JSON body, orIdempotency-KeyHTTP header
Behavior:
- the key is bound to the project, calling API-key actor, MCP server, tool, semantic request, workflow and session context, and whether streaming was requested
- a non-stream retry may replay the stored result only after Keel performs a fresh authorization check for the retrying actor
- the same key with different bound request content returns
409 conflict - after a complete streaming response has been stored, an exact retry replays the same event-stream bytes and original Permit reference without another upstream tool call
- a concurrent or incomplete streaming claim returns
409; an outcome-unknown call requires reconciliation using its original arguments and key
Why the fields differ
Permit creation uses a body field. Execution routes use a header. Use the documented field for each route family.
Related pages
Last updated on Edit this page on GitHub