# Rate limits

Per-key and per-org limits, Retry-After, and correct backoff

Rate limits cap how many requests a key may make in a window; past it, the API answers
429 `RATE_LIMITED`.

Calls draw on one of two budgets. **Sends and tracked events** —
`POST /v1/transactional/messages` (and `/batch`), `POST /v1/push/messages`,
`POST /v1/events/track` (and `/v1/events/batch`) — have their own, larger one, so a burst
of receipts never starves the calls that manage your contacts and campaigns, and the
reverse. Every other call shares the general budget.

| Limit | General | Sends and events | Scope |
| --- | --- | --- | --- |
| Per API key | 300 / minute | 3,000 / minute | One key |
| Per organization | 1,200 / minute | 6,000 / minute | All keys in the organization combined |

A flood guard of 72,000 requests a minute per IP sits above both, so one server can call
for many organizations. A request must pass every limit that applies to it.

Defaults are configurable per deployment, so treat these as the shape of the system rather
than guaranteed numbers. Do not hard-code them in a client; read `Retry-After` instead.

The per-organization limit is the one that surprises people: splitting work across several
keys does not buy more throughput, because they share the organization budget. Keys are
for separating *authority* and rotation, not for raising limits.

## Being limited

Exceeding a limit returns **429** with code `RATE_LIMITED`:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "API request limit exceeded",
    "details": { "retryAfterSeconds": 12 }
  }
}
```

The delay appears twice on purpose. `Retry-After` is the conventional HTTP header that
proxies, HTTP clients and generic retry middleware already understand;
`details.retryAfterSeconds` is the same number where structured error handling can reach
it without parsing headers. **They always agree.** Prefer the header.

## Seeing what is left

Every response to an API-key request carries what is left of the budget it drew on, by
the tighter limit (the key's or the organization's), for the current minute:

```http
RateLimit-Limit: 300
RateLimit-Remaining: 287
RateLimit-Reset: 41
```

`RateLimit-Reset` is seconds until the window starts over. Pace a batch job by
`RateLimit-Remaining` and you will rarely see a 429 at all.

## Backing off correctly

1. On 429, wait at least `Retry-After` seconds. Do not retry sooner — an early retry is
   refused and counts against the limit again.
2. Add jitter. Many workers that all woke on the same limit and all slept the same
   interval will collide again on the same second.
3. Cap total attempts and surface the failure rather than retrying forever.
4. Apply exponential backoff on 5xx, where no `Retry-After` is given.

A reasonable default: up to 3 retries, `Retry-After` when present, otherwise
`2^attempt` seconds, each with ±25% jitter.

Only retry operations that are safe to repeat — see [Idempotency](/docs/api/idempotency). All
`GET` requests are safe. A `POST` without an idempotency key is not.

## Staying under the limit

- **Batch.** `POST /v1/groups/{id}/contacts` adds many contacts in one call. Adding them
  one per request is both slower and the usual cause of hitting a limit.
  `POST /v1/transactional/messages/batch` takes up to 100 sends, but counts as one request per
  send: it saves round trips, not budget.
- **Raise `limit` when paginating.** Reading 100 at a time costs a fifth of the requests
  that reading 20 at a time does.
- **Do not poll for completion.** Sends are asynchronous; subscribe to
  [webhooks](/docs/api/webhooks) instead of asking repeatedly whether a campaign has finished.
- **Bound your concurrency.** One worker respecting the limit is better than twenty
  workers discovering it simultaneously.

A 503 `SERVICE_UNAVAILABLE` from the rate limiter itself means the limiter could not be
reached, not that you were limited. Back off and retry.

## Next steps

- [Errors](/docs/api/errors) — every code and which to retry
- [Idempotency](/docs/api/idempotency) — make retries after a 429 safe
- [Webhooks](/docs/api/webhooks) — learn outcomes without polling
- [Authentication](/docs/api/authentication) — keys and scopes