# Avox API Rate Limits and Retries

> How Avox API rate limits work, what a 429 response means, how to read Retry-After, and a safe retry strategy with backoff.

Source: https://docs.theavoxai.com/avox-api/rate-limits

The Avox API protects every account - and your WhatsApp number's standing with Meta - by limiting
how fast messages can be sent. When you go over a limit you get a `429` response with a
`Retry-After` header that tells you exactly how many seconds to wait.

## The limits

| Limit | What it counts | Error when you hit it |
| --- | --- | --- |
| Per key | Requests made with one API key, over a short rolling window. | [`rate_limited`](https://docs.theavoxai.com/api/errors#rate_limited) |
| Per account | Requests made with all of a Switchboard's keys together. | [`rate_limited`](https://docs.theavoxai.com/api/errors#rate_limited) |
| Per address | Requests from one IP address, plus a tighter limit on repeated wrong keys from one address. | [`rate_limited`](https://docs.theavoxai.com/api/errors#rate_limited) |
| Key per-minute rule | Set on some keys by Avox support, for example at your request. | [`rate_limited`](https://docs.theavoxai.com/api/errors#rate_limited) |
| Key daily allowance | Set on some keys by Avox support. Counts requests per UTC day and resets at midnight UTC. | [`quota_exceeded`](https://docs.theavoxai.com/api/errors#quota_exceeded) |
| WhatsApp's own limits | How fast Meta lets this number send, and how often you can message one person. | [`meta_rate_limited`](https://docs.theavoxai.com/api/errors#meta_rate_limited) |

The limits are generous for normal transactional traffic. They matter most for bulk sends - a loop
that fires thousands of requests at once will hit them. Spread large sends out over time instead.

Every request counts towards these limits, including ones that fail validation, so fix failing
requests rather than retrying them in a tight loop.

> **Note**
>
> Your plan doesn't cap API sends - API messages don't use automation credits. WhatsApp's own
> messaging limits still apply to every message your number starts. See
> [Quality and limits](https://docs.theavoxai.com/whatsapp/quality-and-limits).

## Test keys

[Test keys](https://docs.theavoxai.com/avox-api/testing#1-test-mode) (`avx_test_...`) are counted separately from live keys,
so a test suite never uses up your live allowance, and live traffic never slows your tests:

| Limit | What it counts | Error when you hit it |
| --- | --- | --- |
| Per test key | Requests made with one test key, over a short rolling window. | [`rate_limited`](https://docs.theavoxai.com/api/errors#rate_limited) |
| All of a Switchboard's test keys together | Requests made with every test key in the Switchboard, over a short rolling window. | [`rate_limited`](https://docs.theavoxai.com/api/errors#rate_limited) |
| Daily test allowance | Requests made with every test key in the Switchboard per UTC day. Resets at midnight UTC. | [`quota_exceeded`](https://docs.theavoxai.com/api/errors#quota_exceeded) |

The test limits are set for a normal test suite. A loop that runs away will hit them, which keeps a
bug from flooding your API logs.

- Test requests don't count toward any live key's daily allowance or your plan's usage.
- The per-address limits and any per-minute or daily rule Avox support set on that key still apply.
- WhatsApp's own limits never apply - a test send never reaches WhatsApp. To practise handling them,
  send to the [special test number](https://docs.theavoxai.com/avox-api/testing#special-test-numbers) `+15550000005`, which
  returns `meta_rate_limited` with a `Retry-After` header.

## Reading a 429

```
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-Request-Id: req_7a1e0c94d2b35f8e61c0a9d4
Content-Type: application/json
```

```json
{
  "ok": false,
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait the number of seconds in the Retry-After header, then try again.",
    "docs_url": "https://docs.avox.com/api/errors#rate_limited",
    "request_id": "req_7a1e0c94d2b35f8e61c0a9d4"
  }
}
```

`Retry-After` is always a whole number of seconds, at least `1`. For
[`quota_exceeded`](https://docs.theavoxai.com/api/errors#quota_exceeded) it counts down to midnight UTC.

## Which errors to retry

| Retry? | Codes | How |
| --- | --- | --- |
| Yes, after `Retry-After` | `rate_limited`, `idempotency_in_progress` | Wait the header's seconds, then retry with the same `Idempotency-Key`. |
| Yes, after `Retry-After`, with a new key | `meta_rate_limited` | WhatsApp refused this attempt and the refusal is kept against its `Idempotency-Key`, so retry with a new one. Nothing was sent. |
| Yes, after a pause | `internal_error`, network errors and timeouts | Exponential backoff (below), same `Idempotency-Key`. |
| Carefully | `meta_unavailable` | The message may already have been sent. See [Idempotency](https://docs.theavoxai.com/avox-api/idempotency). |
| Next day | `quota_exceeded` | Wait for `Retry-After` (midnight UTC), or ask [Support](https://docs.theavoxai.com/support) about a higher allowance. |
| No - fix first | Everything else (`400`, `401`, `402`, `403`, `404`, `409` conflicts, `413`, `422`) | The same request will fail the same way. Read the code on [API errors](https://docs.theavoxai.com/api/errors). |

## A safe retry loop

Use exponential backoff with jitter: wait longer after each failure, add a little randomness so many
workers don't retry in lockstep, honour `Retry-After` whenever it's present, and give up after a
few attempts.

**curl**

```bash
# curl retries 429 and 5xx responses itself, honouring Retry-After.
# That includes 502 meta_unavailable, so use this only where a rare duplicate is acceptable.
curl -X POST https://api.avox.com/api/messages/send \
  --retry 5 --retry-max-time 120 \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9b4c3c4e-2f0a-4d4e-8f6b-0f1d6a1c2e33" \
  -d '{"to":"15551234567","type":"text","text":{"body":"Your code is ready."}}'
```

**Node.js**

```js
import { randomUUID } from "node:crypto";

const RETRYABLE = new Set(["rate_limited", "meta_rate_limited", "idempotency_in_progress", "internal_error"]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function sendWithRetry(message, { attempts = 5 } = {}) {
  let idempotencyKey = randomUUID(); // one key for every attempt of this message

  for (let attempt = 1; attempt <= attempts; attempt++) {
    let res;
    try {
      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": idempotencyKey,
        },
        body: JSON.stringify(message),
        signal: AbortSignal.timeout(30_000),
      });
    } catch (networkError) {
      if (attempt === attempts) throw networkError;
      await sleep(backoff(attempt));
      continue;
    }

    const data = await res.json();
    if (data.ok) return data;

    if (!RETRYABLE.has(data.error.code) || attempt === attempts) {
      throw new Error(`${data.error.code} (${data.error.request_id}): ${data.error.message}`);
    }
    // WhatsApp's refusal is stored against the key, so this one needs a fresh key.
    if (data.error.code === "meta_rate_limited") idempotencyKey = randomUUID();
    const retryAfter = Number(res.headers.get("Retry-After"));
    await sleep(retryAfter > 0 ? retryAfter * 1000 : backoff(attempt));
  }
}

function backoff(attempt) {
  const base = Math.min(30_000, 1_000 * 2 ** (attempt - 1)); // 1s, 2s, 4s ... up to 30s
  return base / 2 + Math.random() * (base / 2);
}
```

**Python**

```python
import os
import random
import time
import uuid

import requests

RETRYABLE = {"rate_limited", "meta_rate_limited", "idempotency_in_progress", "internal_error"}


def backoff(attempt):
    base = min(30.0, 2 ** (attempt - 1))  # 1s, 2s, 4s ... up to 30s
    return base / 2 + random.random() * base / 2


def send_with_retry(message, attempts=5):
    idempotency_key = str(uuid.uuid4())  # one key for every attempt of this message

    for attempt in range(1, attempts + 1):
        try:
            res = requests.post(
                "https://api.avox.com/api/messages/send",
                headers={
                    "Authorization": f"Bearer {os.environ['AVOX_API_KEY']}",
                    "Idempotency-Key": idempotency_key,
                },
                json=message,
                timeout=30,
            )
        except requests.RequestException:
            if attempt == attempts:
                raise
            time.sleep(backoff(attempt))
            continue

        data = res.json()
        if data["ok"]:
            return data

        error = data["error"]
        if error["code"] not in RETRYABLE or attempt == attempts:
            raise RuntimeError(f"{error['code']} ({error['request_id']}): {error['message']}")
        # WhatsApp's refusal is stored against the key, so this one needs a fresh key.
        if error["code"] == "meta_rate_limited":
            idempotency_key = str(uuid.uuid4())
        retry_after = int(res.headers.get("Retry-After", "0") or 0)
        time.sleep(retry_after if retry_after > 0 else backoff(attempt))
```

`meta_unavailable` is left out of the retryable list on purpose - decide case by case, because the
first attempt may already have reached the customer.

## Sending in bulk

- **Queue, don't burst.** Put messages on a queue and let a small, fixed number of workers send
  from it.
- **Share the limit sensibly.** All of a Switchboard's keys share one account limit, so one busy
  integration can slow another. Give heavy jobs their own pacing.
- **Watch WhatsApp's side too.** Marketing templates to many new people count against your
  number's messaging limit, and a falling quality rating can lower it. Check
  [Health](https://docs.theavoxai.com/health) before and during a large send.
- **Use the warnings.** **Settings > API Keys** shows **Hit its per-minute limit** or **Hit its
  daily limit** on a key that reached one of its own rules.

## See which requests were limited

Every limited request is on the **API logs** page in the dashboard. Pick `rate_limited`,
`quota_exceeded` or `meta_rate_limited` in the error code filter (**Any error code**) and, if you
like, one key or one date range, to see when and how often it happened. **Export CSV** downloads
the same list. See [API logs](https://docs.theavoxai.com/connections/api-keys#api-logs).

The per-address limits are checked before Avox looks up the key, so a request refused by one of
them isn't tied to your account and doesn't appear there. Your own logs of the `429` and its
`X-Request-Id` are the record for those.
