Why
Network failures, timeouts, and client retry policies mean the same write request can arrive at the server more than once. Constants requires everyPOST, PUT, PATCH, and DELETE to carry an Idempotency-Key header
so the server can detect and coalesce retries into a single write.
Without this, a retried POST /v1/run/my-tool could execute your tool
twice, double-charge quota, or create duplicate records. With it, the
second request returns the first response — cached — and the tool runs
exactly once.
This is a hard requirement. Write requests without an
Idempotency-Key header are rejected with 400 Bad Request.How to use it
Generate a fresh UUID v4 per logical user attempt (not per retry) and send it as theIdempotency-Key header:
In code
Rules
| Situation | Server response |
|---|---|
| Header missing or not a UUID v4 | 400 Bad Request |
| Same key + same request body + handler still running | 409 Conflict |
| Same key + same request body + handler finished | Cached response replayed (adds Idempotent-Replay: true header) |
| Same key + different request body | 422 Unprocessable Entity |
| New key | Handler runs; response cached for 24 hours |
- The cache lives 24 hours. After that, reusing the key starts over as a new request.
- Keys are scoped to the authenticated user. Two different API keys (even for the same workspace) have separate idempotency namespaces.
GET,HEAD, andOPTIONSrequests don’t need the header.
MCP
MCPtools/call is not idempotent at the transport layer. Every
invocation runs fresh — there is no server-side cache keyed on the
JSON-RPC id or any derivation of it.
Why: the Stripe-style idempotency pattern only works when the client
generates a unique key per logical operation. MCP clients (ChatGPT,
Claude Desktop) don’t supply an Idempotency-Key header, and their
JSON-RPC id counters reset per conversation — so any server-derived
key would collide across conversations. Collision would turn
same-arguments calls into cache replays, which is wrong for
time-sensitive tools (logs from the past 5 min, current stock price, unread emails).
If your tool produces destructive side effects and must dedup network
retries, implement the dedup inside the tool: check the current
state before writing, or reject a duplicate based on a domain-level key
(e.g. “skip sending email to X with subject Y if already sent in the
last minute”). Tool-level dedup is correct because the tool author
knows which operations are logically equivalent.
Application retries initiated by MCP clients are rare in practice
(transport-level failures are handled by TCP/HTTP retransmit, not
JSON-RPC retry). The occasional duplicate execution trades a few cents
of sandbox time against the correctness of time-dependent tools — a
trade we take intentionally.
Slack
Slack webhooks delivered to Constants are deduplicated using Slack’s ownevent_id as the idempotency key. If Slack redelivers the same event
(at-least-once delivery), Constants ignores the duplicate.
Picking a good key
- Use a UUID v4 or any opaque string that’s unique per logical attempt.
- Generate it before the first request, not inside your retry loop.
- Don’t hash the request body into the key — let the server do that (fingerprint mismatch is how we catch reused keys pointing at different bodies).
- Don’t reuse a key across different endpoints. If you want to call
/v1/run/tool-aand/v1/run/tool-b, use two different keys.
