# Idempotency

Source: https://docs.usetone.ai/idempotency

> Retry any write safely — the same Idempotency-Key returns the first result instead of doing the work twice.

A network timeout tells you nothing about whether the request ran. Send it again and you might buy
a second phone number; don't, and you might have bought none. Idempotency keys settle it: retry as
many times as you like, and exactly one of them does the work.

```bash
curl "$TONE_API/v1/numbers/purchase" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H "Idempotency-Key: 7f3a0c9e-2b41-4d8a-9f10-4c2e6b7d1a55" \
  -H 'content-type: application/json' \
  -d '{"e164":"+918041234567","label":"support line"}'
```

Retry that exact request — same key, same body — and you get the same `201` and the same number
back, plus a header marking it as a replay:

```
Idempotent-Replayed: true
```

No second number is bought, and no second rental starts.

## Generate the key on your side, per logical operation

Use a UUID v4 — `crypto.randomUUID()`, `uuid.uuid4()`, whatever your runtime offers. The rules:

- **One key per operation you intend to happen once**, not per HTTP attempt. Generate it *before*
  the first attempt and reuse it for every retry of that same operation. A fresh key per retry
  defeats the whole mechanism.
- **A new operation gets a new key.** Buying a second number tomorrow is a new operation.
- Keys are scoped to your organization and live for **24 hours**. After that the key is forgotten
  and reusing it starts a new operation, so don't use one as a permanent deduplication record —
  that's what the resource's own `id` is for.

If you're persisting the operation anyway (a job row, an outbox record), store the key on it. Then
a worker that crashes and restarts picks up the same key and finishes the job exactly once.

## Where it is required

Two endpoints **refuse** a request without the header, with a `400` that says so:

| Endpoint | Why it's mandatory |
|---|---|
| `POST /v1/numbers/purchase` | A duplicate purchase is a second number and a second monthly rental — an ongoing liability, not a one-time charge |
| `POST /v1/campaigns/:id/launch` | A duplicate launch is a second pass over your recipient list: real people called twice, and a compliance problem as well as a billing one |

The header is **honored, not required,** everywhere else it applies — most importantly
`POST /v1/calls` and wallet top-ups. Send it there too. There is no downside, and a retried dial
without one is a second phone call to a real person.

Read requests (`GET`) are naturally idempotent and ignore the header.

## The three failure modes

### Same key, same body → replay

You get the original response — same status, same body — with `Idempotent-Replayed: true`. This
includes errors: if the first attempt failed with a `402`, the replay is that same `402`. The first
attempt's outcome is the operation's outcome.

### Same key, different body → `422`

```json
{
  "error": {
    "type": "invalid_request",
    "code": "idempotency_key_reused_with_different_params",
    "message": "This Idempotency-Key was already used with a different request.",
    "doc_url": "https://docs.usetone.ai/errors#idempotency_key_reused_with_different_params",
    "request_id": "req_8f14a2..."
  }
}
```

This is a bug in your code, and we refuse rather than guess. The alternative — silently replaying
the *first* body — is the classic footgun: you ask to buy number B, get number A back, and your
records disagree with reality forever. The fix is a fresh key for the changed request.

### Same key, still running → `409`

```json
{ "error": { "code": "idempotency_key_in_use", "...": "..." } }
```

Two of your workers raced on the same key. Wait a moment and retry: the first request is still
executing, and once it finishes your retry will replay its stored response.

## What the body has to match

The comparison is over the request body, normalized so key order doesn't matter — `{"a":1,"b":2}`
and `{"b":2,"a":1}` are the same request. Whitespace doesn't matter either. Anything else that
differs — a changed value, an added field — is a different request.

## What this does not protect against

Idempotency keys make a *retry* safe. They don't deduplicate two genuinely separate operations that
happen to look alike: if your code calls `POST /v1/calls` twice with two different keys, that's two
calls, and we place both. Deduplicating your own intent is your side of the line — which is exactly
why the key should be generated where the intent is formed, not where the HTTP request is sent.
