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

# Idempotency

> Safely retry money-adjacent POST requests with the X-Idempotency-Key header without risking duplicate side effects.

Some Alterscope endpoints trigger side effects you don't want to happen twice — submitting an execution step, running a billed simulation, recording an autonomous decision. If the network drops your connection after the server acted but before you saw the response, a naive retry could double-submit.

Idempotency solves this. Send a unique `X-Idempotency-Key` on each such request, and the API guarantees that retrying with the same key replays the original response instead of re-running the work.

<Note>
  Idempotency is required on mutation, simulator, and execution `POST` endpoints. A request to one of these without an `X-Idempotency-Key` header returns `400`. GET requests don't need it — they're already safe to repeat.
</Note>

## Sending the key

Add the header to every protected `POST`:

```http theme={null}
POST /v2/... HTTP/1.1
Host: api.alterscope.org
Authorization: Bearer sk_live_xxx
X-Idempotency-Key: 3f8a1c2e-7b4d-4e9a-9c1f-2a6b8d0e5f31
Content-Type: application/json
```

Execution endpoints require one additional header, `X-Decision-Trace-ID`, alongside the idempotency key; a missing trace ID returns `400`. See [Errors](/develop/get-started/errors) for the full status mapping.

## Generating a key

A key must be unique to the logical operation you're performing. Two strategies work:

* **Random UUID v4** — generate a fresh key per operation and reuse the *same* key across retries of that one operation. This is the simplest correct approach.
* **Stable business key** — derive the key deterministically from your own request identity (for example, an order ID or a step ID), so independent processes retrying the same logical action converge on the same key.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { randomUUID } from "node:crypto";

  const idempotencyKey = randomUUID(); // reuse this exact value on every retry

  await fetch("https://api.alterscope.org/v2/...", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ALTERSCOPE_API_KEY}`,
      "X-Idempotency-Key": idempotencyKey,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });
  ```

  ```python Python theme={null}
  import os
  import uuid
  import requests

  idempotency_key = str(uuid.uuid4())  # reuse this exact value on every retry

  requests.post(
      "https://api.alterscope.org/v2/...",
      headers={
          "Authorization": f"Bearer {os.environ['ALTERSCOPE_API_KEY']}",
          "X-Idempotency-Key": idempotency_key,
          "Content-Type": "application/json",
      },
      json=payload,
  )
  ```
</CodeGroup>

<Warning>
  Hold the key constant across retries, but change it for a genuinely new operation. The replay cache is also keyed on the request body: sending the same `X-Idempotency-Key` with a *different* body does **not** replay the earlier response — it's treated as a distinct request. This prevents a stale "confirmed" reply from being served for new inputs.
</Warning>

## The replay window

Once a request completes successfully, the API caches its response for **24 hours**. Any retry within that window that reuses the same key (and body) gets the original response back, byte-for-byte, with a header signalling the replay:

```http theme={null}
HTTP/1.1 200 OK
X-Idempotency-Replayed: true
```

Check for `X-Idempotency-Replayed: true` if you need to distinguish a fresh result from a cached one.

Two details worth knowing:

* **Only successful (2xx) responses are cached.** If the original request failed (any non-2xx status), the key is released — a retry with the same key re-runs the operation normally. You don't need to mint a new key after a failure.
* **The cache is request-body sensitive.** Same key, different body → no replay (see the warning above).

## In-flight requests

If you fire a second request with the same key *while the first is still being processed*, the API doesn't run it twice and doesn't block waiting. It returns `409 Conflict` immediately with a `Retry-After` header:

```http theme={null}
HTTP/1.1 409 Conflict
Retry-After: 1
Content-Type: application/json

{
  "error": "duplicate request in flight; retry after a brief delay"
}
```

Wait the suggested interval, then retry with the *same* key. By then the original request has typically finalized, so your retry replays the cached response (`X-Idempotency-Replayed: true`) rather than re-executing.

## Worked example

A client submits an execution step, the connection drops before the response arrives, and the client retries:

<Steps>
  <Step title="First attempt">
    Client `POST`s with `X-Idempotency-Key: 3f8a1c2e-...`. The server runs the handler, performs the side effect, and would return `200` — but the connection drops and the client never sees it. The successful response is now cached against the key for 24 hours.
  </Step>

  <Step title="Retry (same key)">
    The client retries the identical request with the same `X-Idempotency-Key`. The server finds the cached response and replays it:

    ```http theme={null}
    HTTP/1.1 200 OK
    X-Idempotency-Replayed: true
    ```

    The side effect ran exactly once. The client sees the original result.
  </Step>

  <Step title="If the retry races the original">
    If the retry arrives while the first request is still in flight, the client instead gets `409 Conflict` with `Retry-After: 1`. It waits one second and retries again — now hitting the cached response from step 2.
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Errors" icon="triangle-exclamation" href="/develop/get-started/errors">
    Status codes and error envelope for failed requests.
  </Card>

  <Card title="Rate limits" icon="gauge-high" href="/develop/get-started/rate-limits">
    Per-tier request and quota limits.
  </Card>
</CardGroup>
