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

# Idempotency

> Retry writes safely — send an Idempotency-Key and a repeat replays the original response instead of doing the work twice.

Every write endpoint accepts an `Idempotency-Key` header. Send one and a
retried request replays the original response instead of performing the
operation again.

This matters because the case where your client times out is exactly the case
where the request may already have succeeded. Without an idempotency key, a
retried `POST /v1/meetings` creates a second meeting, spawns a second agent,
and bills you for both — and nothing on your side can tell you it happened.

```bash theme={null}
curl -X POST https://api.waterr.ai/v1/meetings \
  -H "Authorization: Bearer wai_your_key" \
  -H "Idempotency-Key: 8f14e45f-ea0c-4b3f-9f7a-2b6c1d0e5a91" \
  -H "Content-Type: application/json" \
  -d '{"scenario_id": "..."}'
```

## Choosing a key

Any unique string up to 255 characters. A UUID per logical operation is the
usual choice. **Generate it before the first attempt and reuse it for every
retry of that same operation** — a key generated inside the retry loop
protects nothing.

<Warning>
  Do not reuse a key for a different operation. A key is bound to the exact
  request that created it; reusing it with a different body is rejected, not
  silently reinterpreted.
</Warning>

## Behaviour

| Situation | Result |
| - | - |
| First request with this key | Executes normally. Response is stored for **24 hours**. |
| Repeat, same key and body | Original response replayed verbatim, with `Idempotency-Replayed: true`. The operation does **not** run again. |
| Repeat, same key, **different** body | `422` `idempotency_key_reuse`. |
| Repeat while the first is still running | `409` `idempotency_key_in_use` with `Retry-After`. |
| First request failed (4xx/5xx) | The key is released, so retrying the same operation with the same key is allowed. |

Only successful (2xx) responses are stored. A failed write does not "use up"
its key — otherwise a transient validation failure would lock you out of
retrying the corrected request.

Keys are scoped to your API key. Two workspaces choosing the same key string
never collide.

## Detecting a replay

```javascript theme={null}
const res = await fetch("https://api.waterr.ai/v1/meetings", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    "Idempotency-Key": operationId,
  },
  body: JSON.stringify({ scenario_id: scenarioId }),
});

if (res.headers.get("Idempotency-Replayed") === "true") {
  // This meeting already existed — the earlier attempt did succeed.
}
```

## A complete retry loop

Pairs with the backoff described in [Errors](/api-reference/errors) — one key
per operation, generated outside the loop.

<CodeGroup>
  ```python Python theme={null}
  import time, uuid, httpx

  def create_meeting(client, payload):
      key = str(uuid.uuid4())  # once per operation, NOT per attempt

      for attempt in range(5):
          r = client.post(
              "/meetings", json=payload, headers={"Idempotency-Key": key}
          )
          if r.status_code < 400:
              return r.json()
          if r.status_code in (409, 429) or r.status_code >= 500:
              time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
              continue
          r.raise_for_status()

      raise RuntimeError("exhausted retries")
  ```

  ```javascript Node theme={null}
  import { randomUUID } from "node:crypto";

  async function createMeeting(payload) {
    const key = randomUUID(); // once per operation, NOT per attempt

    for (let attempt = 0; attempt < 5; attempt++) {
      const res = await fetch("https://api.waterr.ai/v1/meetings", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.WATERR_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": key,
        },
        body: JSON.stringify(payload),
      });

      if (res.ok) return res.json();

      if (res.status === 409 || res.status === 429 || res.status >= 500) {
        const wait = Number(res.headers.get("Retry-After") || 2 ** attempt);
        await new Promise((r) => setTimeout(r, wait * 1000));
        continue;
      }

      throw new Error((await res.json()).error?.message ?? "request failed");
    }

    throw new Error("exhausted retries");
  }
  ```
</CodeGroup>

## Scope

Applies to `POST`, `PUT`, `PATCH` and `DELETE` on the public API. `GET` needs
no key — reads are already idempotent, and the header is ignored there.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.