Rate limits
One limiter covers /api/v1. It counts requests per minute, per API key.
The numbers
| Caller | Limit | Counted by |
|---|---|---|
| An authenticated request | the organization's plan limit, or 300/min when the plan sets none | the API key's id |
| An unauthenticated request | 60/min | the client IP |
The default of 300 comes from callagent.limits.api_rate_limit_per_minute and can be changed per deployment with the API_RATE_LIMIT environment variable.
The bucket is keyed on the key, not the organization. Two keys in one organization get two independent budgets, which is the cheapest way to stop a chatty background job from starving your checkout flow: give it its own key.
The 60/min anonymous bucket is effectively unreachable on /api/v1
Authentication runs before the limiter, so a request with no key is rejected with 401 before it is ever counted. The anonymous bucket exists for surfaces that are mounted without the key middleware.
Per-plan limits
A plan can raise or lower the ceiling. The limit is an entitlement on the plan, so it follows the organization rather than the key.
| Plan | Requests / minute |
|---|---|
| Starter | 60 |
| Growth | 300 |
| Enterprise | 1200 |
| No plan | 300 (the platform default) |
An organization with no plan is not unlimited — it falls back to the platform default. Individual organizations can be given an override without changing plan.
You can read your own ceiling and how much of it you have used from GET /billing/plans, which returns a limits object with limit, used and unlimited for every enforced entitlement. That is worth checking before a bulk import, rather than discovering the ceiling as a 402 halfway through one.
Headers
Every response through the limiter carries the budget:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287A 429 carries two more:
Retry-After: 34
X-RateLimit-Reset: 1767268800Retry-After is seconds to wait. X-RateLimit-Reset is an absolute Unix timestamp — the two are different units, which is the usual way to get this wrong.
The 429 body
{
"error": {
"type": "rate_limit_error",
"message": "Too many requests. Slow down and retry after the interval in the Retry-After header.",
"retryable": true,
"documentation_url": "https://docs.callagent.dev/api/rate-limits",
"request_id": "01JQ…"
}
}Handling it
Sleep for Retry-After and try again. Do not compute your own backoff from the status code — the server knows when the window resets and you do not.
async function callWithRetry(url, init, attempts = 5) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
const response = await fetch(url, init);
if (response.status !== 429) return response;
// Seconds, not milliseconds, and not a timestamp.
const wait = Number(response.headers.get('Retry-After') ?? 1);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
throw new Error('Rate limited after every attempt.');
}<?php
function call_with_retry(callable $send, int $attempts = 5): array
{
for ($attempt = 0; $attempt < $attempts; $attempt++) {
[$status, $headers, $body] = $send();
if ($status !== 429) {
return [$status, $headers, $body];
}
// Seconds, not milliseconds, and not a timestamp.
sleep((int) ($headers['retry-after'] ?? 1));
}
throw new RuntimeException('Rate limited after every attempt.');
}# --retry 5 handles 429 and 5xx; --retry-delay is a floor, curl honours Retry-After.
curl --retry 5 --retry-delay 1 --retry-all-errors \
-X GET "$CALLAGENT_API_URL/calls" \
-H "Authorization: Bearer $CALLAGENT_API_KEY"Staying under it
Read X-RateLimit-Remaining and slow down before you hit zero. A client that only reacts to 429 spends part of every minute rate-limited.
Use one key per workload. The bucket is per key, so a batch job and an interactive integration on separate keys cannot starve each other.
Follow links.next instead of polling. A list endpoint returns a paginator; walking it costs one request per page, while re-fetching page one in a loop costs one per poll.
Do not poll for call state. Webhooks push call.* events; a poll loop against GET /calls/{call} is the single most common way an integration exhausts its budget.
Other limits, which are not this one
These share the word "limit" and nothing else:
- Plan resource ceilings — how many assistants, queues, campaigns, knowledge bases, tools, API keys, phone numbers and operator seats you may have. Exceeding one is a
402witherror.type: "plan_limit_exceeded"anderror.codenaming the entitlement (max_assistants,max_api_keys,phone_numbers, …), pluslimitandused. - Concurrency — how many calls may be in progress at once. Exceeding it refuses the call, not the request.
- Provider rate limits — a transcription or model provider throttling us. That surfaces as
error.type: "provider_error"witherror.code: "rate_limited"and its ownRetry-After, which is a fixed 5 seconds rather than a computed window. - Per-endpoint page sizes —
limiton Engage endpoints andper_pageon Platform and Billing endpoints, each capped at 100. Not a rate limit; just a ceiling on one response.