# Avox API Idempotency

> Use the Idempotency-Key header to retry Avox API requests safely without ever sending the same WhatsApp message twice.

Source: https://docs.theavoxai.com/avox-api/idempotency

Networks fail. A request can time out after WhatsApp has already accepted the message, and a plain
retry would send it a second time. The `Idempotency-Key` header stops that: retry with the same
key and the same body, and Avox returns the first result instead of sending again.

## How to use it

**1. Make one key per message**

Generate a new value for each message you intend to send - a UUID is ideal, or an id from your
own system such as `order-A-1042-shipped`.

**2. Send it as a header**

`Idempotency-Key: 6f1d2c1e-8a4b-4c7e-9a51-3b0f2d9e7c10`

**3. Store it with the job**

Save the key next to the message in your queue or database, so every retry of that message
reuses it.

**4. Retry with the same key and the same body**

If a request times out or fails with a retryable error, send it again unchanged.

**curl**

```bash
curl -X POST https://api.avox.com/api/messages/send \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-A-1042-shipped" \
  -d '{"to":"15551234567","type":"template","template":{"name":"order_shipped","language":{"code":"en_US"}}}'
```

**Node.js**

```js
// The key comes from your own job record, so a retried job reuses it.
async function sendShippedNotice(order) {
  const res = await fetch("https://api.avox.com/api/messages/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.AVOX_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `order-${order.id}-shipped`,
    },
    body: JSON.stringify({
      to: order.phone,
      type: "template",
      template: { name: "order_shipped", language: { code: "en_US" } },
    }),
  });
  const replayed = res.headers.get("Idempotent-Replayed") === "true";
  return { status: res.status, replayed, body: await res.json() };
}
```

**Python**

```python
import os

import requests


def send_shipped_notice(order):
    res = requests.post(
        "https://api.avox.com/api/messages/send",
        headers={
            "Authorization": f"Bearer {os.environ['AVOX_API_KEY']}",
            "Idempotency-Key": f"order-{order['id']}-shipped",
        },
        json={
            "to": order["phone"],
            "type": "template",
            "template": {"name": "order_shipped", "language": {"code": "en_US"}},
        },
        timeout=30,
    )
    replayed = res.headers.get("Idempotent-Replayed") == "true"
    return res.status_code, replayed, res.json()
```

The header is optional. Without it, every request is treated as new. It's strongly recommended for
anything sent from a queue, a scheduled job, a webhook handler, or any code that retries on its
own.

## The rules

| Rule | Detail |
| --- | --- |
| Header name | `Idempotency-Key` |
| Format | 1 to 255 visible ASCII characters, no spaces. Anything else returns [`invalid_idempotency_key`](https://docs.theavoxai.com/api/errors#invalid_idempotency_key). |
| Scope | Per API key. The same value used with two different keys counts as two different keys, so separate keys (and separate customers) can never collide. A rotated key's replacement is a different key too. |
| Window | A finished result is kept for 24 hours. After that the same value counts as new. |
| "Same body" | Compared after Avox reads the message. Key order, spacing, and the format of `to` (`+1 555-123-4567` vs `15551234567`) don't matter; fields Avox drops don't count. |

## What you get back

| Situation | Response |
| --- | --- |
| First time Avox sees the key | Processed normally. |
| Same key, same body, first request finished | The first request's status and body, unchanged, with the header `Idempotent-Replayed: true`. Nothing is sent again. |
| Same key, same body, first request still running | `409` [`idempotency_in_progress`](https://docs.theavoxai.com/api/errors#idempotency_in_progress) with a short `Retry-After`. Wait, then retry with the same key. |
| Same key, different body | `409` [`idempotency_conflict`](https://docs.theavoxai.com/api/errors#idempotency_conflict). Nothing is sent. |

A replayed body still holds the **first** request's `request_id` and `message_id`. The
`X-Request-Id` header on the replay is new, so you can tell the two calls apart in your logs.

In the dashboard, every replay is listed as its own request:

- On the **API logs** page, a replayed success shows **replayed** next to its status, and its
  **Request details** say "Replayed from an earlier request with the same Idempotency-Key -
  nothing was sent again."
- In a key's **Recent requests** in **Settings > API Keys**, it shows as **Replayed**.
- A replayed error shows its original error code in both places.

Test keys follow exactly the same rules, so you can check your retry logic in
[test mode](https://docs.theavoxai.com/avox-api/testing#1-test-mode) before going live.

## Which results are kept

Not every outcome is stored against the key. This decides what a retry does.

| First outcome | Stored? | What a retry with the same key does |
| --- | --- | --- |
| Sent (`200`) | Yes | Returns the same success. No second message. |
| Refused by WhatsApp (`422`, or `429` [`meta_rate_limited`](https://docs.theavoxai.com/api/errors#meta_rate_limited)) | Yes | Returns the same error. To send a corrected message, use a **new** key. |
| [`meta_unavailable`](https://docs.theavoxai.com/api/errors#meta_unavailable) (`502`) | No - the key is freed | Runs as a new request and **will send**. See the warning below. |
| [`connection_not_ready`](https://docs.theavoxai.com/api/errors#connection_not_ready) | No - the key is freed | Runs as a new request once the number is reconnected. |
| Refused before the idempotency check (bad key, limits, invalid body, and so on) | No | Runs as a new request, so you can fix the problem and retry with the same key. |
| [`internal_error`](https://docs.theavoxai.com/api/errors#internal_error) (`500`) | No | May return `idempotency_in_progress` for a couple of minutes, then runs as new. |

> **Warning**
>
> `meta_unavailable` means WhatsApp didn't answer in time, so nobody knows whether the message went
> out. Avox frees the key so you decide what to do. If a duplicate would matter (a payment
> reminder, a one-time code), check [Logs](https://docs.theavoxai.com/logs) for a status update on the message, or ask the
> customer, before sending again.

## Common mistakes

- **One fixed key for everything.** The second message gets `idempotency_conflict`. Make a new key
  per message.
- **A new key on every retry.** Then the header does nothing. Generate the key once, when the
  message is created, and reuse it.
- **Changing the body on retry.** Adding a timestamp or a random value to the body makes it a
  different message, which conflicts. Keep retries byte-for-byte the same message.
- **Reusing a key after fixing a WhatsApp refusal.** The refusal is stored for 24 hours. Use a new
  key for the corrected message.
- **Switching API keys mid-retry.** Idempotency keys belong to one API key. After a
  [rotation](https://docs.theavoxai.com/avox-api/authentication#rotating-a-key), a retry sent with the new API key runs as a
  new request, so let pending retries finish on the old key first.

See [Rate limits](https://docs.theavoxai.com/avox-api/rate-limits) for which errors to retry and how long to wait.
