Rate limits & quotas
Rate limits & quotas
Every ceiling a programmatic caller can hit, with the exact number, what it is counted against, and the status you get at the cap. If you are handling a 429 and want to know which limit produced it, this is the page.
The four kinds of limit
They are independent — a request has to clear every one that applies to it.
| Kind | Counts | Window | At the cap |
|---|---|---|---|
| HTTP rate limit | requests, per credential or IP | 15 min (5 min for webhooks) | 429 + Retry-After |
| Per-key quota | requests and rows, per API key | 1 hour | 429 + Retry-After, message names the dimension |
| Per-request cap | the size of one request (targets, ids, page size) | — | 400, or silently clamped for page sizes |
| Concurrency | calls live at the same moment | — | 429 + Retry-After, body carries scope/cap/active |
The first two are runaway guards — they are set far above any sane integration and are not pricing levers. Concurrency is the one most people actually hit; it is about live calls, not requests.
HTTP rate limits
Applied to every request before it reaches a handler.
| Traffic | Limit | Counted per |
|---|---|---|
| Signed-in dashboard/SDK traffic (JWT) | 3000 requests / 15 min | user |
API-key traffic — /api/* and /api/v1/* | 2000 requests / 15 min | client IP |
Public endpoints (/api/public/*) | 3000 requests / 15 min | client IP |
Sign-in & token refresh (/api/auth/*) | 600 requests / 15 min | client IP |
Carrier & app webhooks (/webhooks/*) | 500 requests / 5 min | client IP |
The bucket follows the credential, not the path. A request carrying a valid user JWT is counted against that user, whatever it calls. A request carrying an X-API-Key — the only credential the Integration API accepts — is counted against the calling IP address. Two consequences worth designing around:
- API keys do not each get their own window. Splitting one integration into three keys on the same server does not triple your request budget (it does give each key its own hourly quota, which is a different thing).
- Everything behind one egress address shares one window — your own jobs, and anything else calling from the same NAT or office network.
Sign-in has a separate bucket on purpose: a data-plane window you have drained can never lock you out of /api/auth/login or /api/auth/refresh.
Health endpoints (/health, /health/live, /health/ready) sit outside /api and are not rate-limited.
Per-API-key quotas
Hourly quotas on the bulk campaign and connector endpoints of the Integration API, counted per API key rather than per IP — so your nightly job, which arrives from a single address, gets its own budget and cannot be crowded out by anything else on that address.
| Endpoint | Quota (per key, per hour) | Self-hosted override |
|---|---|---|
POST /api/v1/campaigns/{id}/targets | 1,200 requests and 200,000 rows | PUSH_REQS_PER_HOUR, PUSH_ROWS_PER_HOUR |
GET /api/v1/campaigns/{id}/results | 1,200 requests (own counter) | PUSH_REQS_PER_HOUR |
POST /api/v1/campaigns | 60 campaigns, plus its targets charged to the row quota above | CAMPAIGN_CREATE_PER_HOUR |
POST · PATCH · PUT · DELETE /api/v1/integrations/connections | 600 writes | INTEGRATION_WRITES_PER_HOUR |
POST /api/v1/integrations/connections/{id}/test, and the verification a create/update runs | 600 verifications (own counter) | INTEGRATION_VERIFIES_PER_HOUR |
Two dimensions, because they fail differently. A runaway loop sends many small requests; a bulk backfill sends few enormous ones. Counting only requests lets the second through, and counting only rows lets the first hammer the endpoint with empty work. 200,000 rows/hour is a 200-page backfill — far above any nightly sync.
Both are charged before the work, so a push that exceeds either budget writes nothing at all — nothing is half-applied, and nothing is lost. Retry after the window and the job continues where it stopped, because pushed targets de-duplicate on your own id.
Connector writes and verifications are counted separately, for the same reason. A write is a row in our database; a verification is a request to someone else's API, made with your stored credential — so a loop that re-verifies spends your rate limit at the vendor and can get your key throttled or flagged at their end. A POST /integrations/connections with verification left on spends one of each.
Everything else on /api/v1 — me, hooks, agents, numbers, calls, events/sample, chat, and the connector reads (GET /integrations/providers, GET /integrations/connections) — has no hourly quota. Only the HTTP rate limit above applies.
Per-request caps
| Cap | Value | Behaviour at the cap |
|---|---|---|
Targets in one POST …/campaigns/{id}/targets | 1000 | Rejected — 400, page the rest in |
Targets in POST /api/v1/campaigns (create + load) | 1000 | Rejected — 400, create then page in |
DNC bulk delete (ids) | 10000 per request | Rejected — 400 |
| DNC CSV export | 500000 rows | Truncated |
| Request body | 2 MB unless the endpoint documents otherwise (uploads are higher) | Rejected — 413 |
Page sizes are clamped, never rejected — an over-large limit silently returns the maximum:
| List endpoint | Default limit | Max |
|---|---|---|
GET /api/v1/agents | 100 | 200 |
GET /api/v1/calls | 50 | 200 |
GET /api/v1/campaigns/{id}/results | 500 | 1000 |
| Dashboard list endpoints | commonly 50–100 | commonly 200 |
Concurrency
A 429 that is not about request rate: how many calls may be live at once. Three ceilings apply to every call — per number (default 2), per workspace (yours to set, unset by default), and browser (default 50, web calls only). The body tells you which one you hit:
{
"success": false,
"error": "This number is already on its limit of 2 simultaneous calls (2 live). Retry in about 240s, or send bulk calls through a campaign so they are paced for you.",
"retryAfter": 240,
"reason": "number_at_capacity",
"scope": "number",
"cap": 2,
"active": 2,
"position": 3,
"medianCallSecs": 120
}
scope is number, org or browser — it decides who fixes it (number and browser are platform-admin settings, org is yours under Usage → Edit limits).
This is the limit behind "my burst of 200 POST /initiate-call requests only placed two calls". That endpoint places one call; for bulk dialling use a campaign, which takes the whole list and paces it. Full picture and configuration: Concurrency limits.
Public & sign-in endpoints
| Limit | Value | At the cap |
|---|---|---|
Widget session-init (/api/public/widget/{slug}/session) | 10 starts / 60 s, per slug + IP | 400 "Too many attempts — please wait a moment and try again." |
| Failed sign-ins, per account | 10 / 15 min | 429 — counts failures only; a successful sign-in clears it |
| Failed sign-ins, per IP | 50 / 15 min | 429 — catches one password sprayed across many accounts |
| Client token TTL | default 600 s, range 30 s–3600 s | Clamped |
Client token maxCalls | ceiling 100 | Clamped |
What a 429 looks like
Every rate-limit and quota response carries a Retry-After header and a retryAfter field, both in seconds, both the real time left on the window:
HTTP/1.1 429 Too Many Requests
Retry-After: 812
Content-Type: application/json
{
"success": false,
"error": "quota exceeded: at most 1200 per 60 minute(s) — retry in 812s",
"retryAfter": 812
}
A plain rate-limit 429 uses the generic message "Too many requests, please try again later". A quota 429 names the dimension you exceeded — requests or rows — because a caller who sent few but very large requests needs to know it is rows, not to tune its request rate.
Staying under the limits
- Honour
Retry-After. Do not guess a backoff: an hourly window means a client guessing 30-second retries burns its budget in the first minute and reports a failure it could have described precisely. - Push in pages of ≤ 1000 and pull results by watermark. Results arrive continuously, so
offsetpaging silently skips rows that landed between two polls — storenextSincefrom each response and pass it back. - Dial in bulk with campaigns, never with parallel
/initiate-call. One request hands over the whole list; the platform paces it, retries no-answers and reports every outcome. - Cache the slim lists.
GET /api/v1/agentsand/api/v1/numbersexist to fill dropdowns — fetch once per run, not once per row. - Retry safely. Send an
Idempotency-Keyon POSTs: a repeat of the same key replays the original result for 24 hours instead of acting twice. See Idempotency. - Treat these as ceilings, not as a contract. Limits are enforced on a best-effort basis and the exact numbers may be raised or tuned per deployment; design the client to back off on any
429rather than to depend on one arriving.
Need a higher ceiling for a genuine workload? Self-hosted deployments set the environment variables listed above. On the hosted platform, ask your platform admin — concurrency caps in particular are per-organization settings.
Related
- Overview & conventions — surfaces, errors, idempotency, pagination
- Concurrency limits — the three call ceilings and how to configure them
- Campaigns API — bulk dialling, pushed sources, result pulls
- Usage & billing — monthly quotas, which are a different thing from rate limits
- Limits & quotas (App Platform) — the separate ceilings that apply to app-key APIs