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 clients don’t need to send Idempotency-Key explicitly. The server uses the JSON-RPC id field — which every MCP client already sends and reuses on retry — to dedupe tools/call invocations automatically.

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.