Skip to main content

Why

Network failures, timeouts, and client retry policies mean the same write request can arrive at the server more than once. Constants requires every POST, 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 the Idempotency-Key header:
curl -X POST https://constants.io/api/v1/run/process_csv_data_abc12345 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 3f9b2c1a-6e4d-4b8e-9a7d-2f8c1e5b4a9d" \
  -H "Content-Type: application/json" \
  -d '{"filter_column":"status","filter_value":"active"}'
If your HTTP client retries that request for any reason — timeout, connection reset, 502 from a proxy — reuse the same key. The server will return the cached response and your tool won’t run again.

In code

import { randomUUID } from "crypto";

const idempotencyKey = randomUUID();

async function runTool() {
  return fetch("https://constants.io/api/v1/run/my_tool", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CONSTANTS_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({ input: "..." }),
  });
}

// Retries of this same logical attempt reuse the same key.
await retry(runTool, { retries: 3 });

Rules

SituationServer response
Header missing or not a UUID v4400 Bad Request
Same key + same request body + handler still running409 Conflict
Same key + same request body + handler finishedCached response replayed (adds Idempotent-Replay: true header)
Same key + different request body422 Unprocessable Entity
New keyHandler 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, and OPTIONS requests don’t need the header.

MCP

MCP tools/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 own event_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-a and /v1/run/tool-b, use two different keys.

When a retry is not safe

The idempotency system protects against duplicate executions. It does not protect against partial failures where the server completed the write but the response never reached you. That’s exactly the case idempotency is designed for: retry with the same key, get the cached response, know the write happened. It also can’t protect against writes that failed before the server saw them (e.g. DNS failures, connection refused). Those are safe to retry with the same or a new key — the server never recorded anything.