# Retrying safely after a timeout

Source: https://docs.usetone.ai/flows/idempotent-retry

> A timeout tells you nothing about whether the work happened. This is how you find out without doing it twice.

Three operations spend money or dial a stranger, and for those a timeout is the
dangerous case — not because it failed, but because you cannot tell whether it
did.

| Operation | `Idempotency-Key` |
|---|---|
| `POST /v1/numbers/purchase` | **Required** — `400` without it |
| `POST /v1/campaigns/{id}/launch` | **Required** — `400` without it |
| `POST /v1/calls` | Optional, strongly recommended |

## The rule

Generate a key per **logical** operation, not per attempt. Retry with the *same*
key.

```js
const key = crypto.randomUUID();          // once, outside the retry loop

async function buy(e164) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(`${API}/v1/numbers/purchase`, {
      method: 'POST',
      headers: {
        authorization: `Bearer ${process.env.TONE_KEY}`,
        'content-type': 'application/json',
        'idempotency-key': key,           // ← the SAME key every attempt
      },
      body: JSON.stringify({ e164 }),
    });

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

    const { error } = await res.json();
    if (error.code === 'idempotency_key_in_use') {   // 409 — first one still running
      await sleep(1000 * 2 ** attempt);
      continue;
    }
    if (res.status >= 500 || error.code === 'provider_unavailable') {
      await sleep(1000 * 2 ** attempt);
      continue;
    }
    throw new Error(`${error.code}: ${error.message}`);
  }
}
```

Persist the key alongside whatever prompted the operation. A key held only in
memory is gone exactly when the process crashed mid-request — the one case it
existed for.

## What each response means

| | |
|---|---|
| `2xx` with `Idempotent-Replayed: true` | The first attempt succeeded. This is its stored response — not new work |
| `409 idempotency_key_in_use` | The first attempt is still running. Wait and retry |
| `422 idempotency_key_reused_with_different_params` | Same key, different body. Almost always a bug: a key reused across two genuinely different operations |
| `503 provider_unavailable` | 🔴 The carrier's answer left the outcome **unknown** — not "it failed". Retry with the **same** key. A fresh key risks buying two numbers |

Keys are remembered for **24 hours**. A handler that throws releases its key, so
a genuine error can be retried after fixing the request.

## Where it does not help

Idempotency protects the operation, not your bookkeeping. If you generate a new
key because your own record of the first attempt was lost, you get two numbers
and two rentals. The durable key is the whole mechanism.
