> ## Documentation Index
> Fetch the complete documentation index at: https://docs.constants.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Every write request to the Constants API requires an Idempotency-Key header so retries are always safe.

## 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.

<Note>
  This is a hard requirement. Write requests without an
  `Idempotency-Key` header are rejected with `400 Bad Request`.
</Note>

## How to use it

Generate a fresh UUID v4 per *logical* user attempt (not per retry) and
send it as the `Idempotency-Key` header:

```bash theme={null}
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

<CodeGroup>
  ```ts TypeScript theme={null}
  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 });
  ```

  ```python Python theme={null}
  import uuid, requests
  from tenacity import retry, stop_after_attempt

  idempotency_key = str(uuid.uuid4())

  @retry(stop=stop_after_attempt(3))
  def run_tool():
      return requests.post(
          "https://constants.io/api/v1/run/my_tool",
          headers={
              "Authorization": f"Bearer {API_KEY}",
              "Content-Type": "application/json",
              "Idempotency-Key": idempotency_key,
          },
          json={"input": "..."},
      )

  run_tool()
  ```

  ```bash cURL with retry theme={null}
  KEY=$(uuidgen)
  for i in 1 2 3; do
    curl --retry 0 -X POST https://constants.io/api/v1/run/my_tool \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: $KEY" \
      -H "Content-Type: application/json" \
      -d '{"input":"..."}' && break
    sleep $((2 ** i))
  done
  ```
</CodeGroup>

## 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`, 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.
