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

# Errors, limits, and pagination

> The error vocabulary, rate limits, pagination, and how the API evolves.

## Errors

Errors use conventional status codes with a machine-readable `detail`:

| Status | Meaning                                         | Example `detail`                                                                                                                                                   |
| ------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400    | The request is addressed wrong                  | `{"code": "invalid_channel", …}`, `{"code": "channel_id_required", …}`                                                                                             |
| 401    | The key is missing, unknown, or revoked         | `"missing_bearer_token"`, `"invalid_api_key"`                                                                                                                      |
| 402    | Not enough WhatsApp balance for what you asked  | `{"code": "wa_insufficient_balance", …}`                                                                                                                           |
| 403    | Valid credential, not allowed here              | `"api_key_required"`, `"scale_plan_required"`, `"trial_expired"`, `"subscription_required"` — see [Authentication](/api-reference/authentication#plan-requirement) |
| 404    | The resource doesn't exist in your organization | `"not_found"`, `{"code": "contact_not_found", …}`                                                                                                                  |
| 409    | Conflict                                        | `{"code": "duplicate_contact", …}`, `{"code": "messaging_limit_exceeded", …}`                                                                                      |
| 422    | The request body failed validation              | field-level messages, or `{"code": …}`                                                                                                                             |
| 429    | Over the rate limit                             | `"rate_limited"` — retry after the `Retry-After` header                                                                                                            |

Structured errors always carry a stable `code` you can branch on; extra keys (like `conflicting_contact_id` on a duplicate contact) give you what you need to resolve it. Every error shape is published in the [API reference](/api-reference/openapi).

### Sending a template

`POST /v1/templates/{id}/send` refuses the **whole** request rather than queuing part of it, so a rejection never leaves you half-sent and never costs you anything. The codes worth branching on:

| `code`                                                           | Status    | What to do                                                                                                                                                                             |
| ---------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channel_id_required`                                            | 400       | Your account has more than one WhatsApp number. Pick one — the response lists the eligible ids.                                                                                        |
| `invalid_channel`                                                | 400       | The `channel_id` isn't a live number on this template's WhatsApp account.                                                                                                              |
| `template_not_sendable`                                          | 409 / 422 | The template isn't `APPROVED` (409), or it can never be sent — a carousel card's button has no way to be filled (422).                                                                 |
| `messaging_limit_exceeded`                                       | 409       | The batch is bigger than what's left of WhatsApp's rolling 24-hour limit. `remaining` says how many would fit.                                                                         |
| `wa_insufficient_balance`                                        | 402       | Top up the WhatsApp wallet; nothing was queued.                                                                                                                                        |
| `contact_not_found`                                              | 404       | `index` says which recipient.                                                                                                                                                          |
| `missing_variables`                                              | 422       | `tokens` lists what that recipient still needs — including `header.media` when the template has a media header and you supplied no URL.                                                |
| `unknown_variables` / `unknown_url_suffixes`                     | 422       | You sent a key the template doesn't use. It would have been silently dropped, so we refuse instead.                                                                                    |
| `media_too_large` / `media_type_mismatch` / `media_fetch_failed` | 422       | We fetch `header_media_url` ourselves; it must be publicly reachable, of the header's type, and within WhatsApp's size limit for that type (5 MB image, 16 MB video, 100 MB document). |
| `idempotency_key_in_flight`                                      | 409       | An earlier request with this `Idempotency-Key` is still being queued.                                                                                                                  |

`GET /v1/templates/{id}/send-requirements` tells you what a template needs **before** you send, so most of these never have to happen.

## Rate limits

Each organization has a shared budget of **20 requests per second** across all its keys — **5 per second** during the free trial. Over the budget, requests return `429` with a `Retry-After` header (seconds; always `1`, since the window is one second).

Every response — including the `429` — carries your current budget, so you can pace without counting calls yourself:

| Header                  |                                                         |
| ----------------------- | ------------------------------------------------------- |
| `X-RateLimit-Limit`     | Requests allowed per window.                            |
| `X-RateLimit-Remaining` | Requests left in the current window.                    |
| `X-RateLimit-Reset`     | When the window resets, as a Unix timestamp in seconds. |

Read `X-RateLimit-Remaining` and slow down as it approaches zero rather than sprinting into the `429`. If your integration genuinely needs more, talk to us.

<Note>
  One request is one request no matter how much it does. `POST /v1/templates/{id}/send` counts once whether it carries one recipient or a hundred — so batching recipients into a single send is the cheapest way to stay inside the budget. What bounds actual message volume is WhatsApp's own rolling 24-hour limit, not this one.
</Note>

## Retrying safely

`POST /v1/templates/{id}/send` accepts an **`Idempotency-Key`** header. Send the same key again within 24 hours and you get the original broadcast back instead of a second send — which is what you want after a timeout, when you genuinely cannot tell whether the request landed. Use a fresh key for each distinct send.

## Pagination

List endpoints return:

```json theme={null}
{ "items": [ … ], "next_cursor": "cnt_…" }
```

Pass `next_cursor` back as `?cursor=` to fetch the next page; a `null` cursor means you've reached the end. Cursors are opaque — don't parse them.

Property and funnel-stage lists aren't paginated: they return your full set, with no `next_cursor`.

## Compatibility

This is an early, fast-moving API — expect it to evolve. Two habits keep your integration stable:

* Most changes are **additive**: new endpoints, new optional parameters, new response fields, new values in string fields. Tolerate unknown fields and values — ignore what you don't recognize.
* Every contract change is listed in the [changelog](/api-reference/changelog) — check it when something looks different.
