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.

KindCountsWindowAt the cap
HTTP rate limitrequests, per credential or IP15 min (5 min for webhooks)429 + Retry-After
Per-key quotarequests and rows, per API key1 hour429 + Retry-After, message names the dimension
Per-request capthe size of one request (targets, ids, page size)400, or silently clamped for page sizes
Concurrencycalls live at the same moment429 + 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.

TrafficLimitCounted per
Signed-in dashboard/SDK traffic (JWT)3000 requests / 15 minuser
API-key traffic — /api/* and /api/v1/*2000 requests / 15 minclient IP
Public endpoints (/api/public/*)3000 requests / 15 minclient IP
Sign-in & token refresh (/api/auth/*)600 requests / 15 minclient IP
Carrier & app webhooks (/webhooks/*)500 requests / 5 minclient 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.

EndpointQuota (per key, per hour)Self-hosted override
POST /api/v1/campaigns/{id}/targets1,200 requests and 200,000 rowsPUSH_REQS_PER_HOUR, PUSH_ROWS_PER_HOUR
GET /api/v1/campaigns/{id}/results1,200 requests (own counter)PUSH_REQS_PER_HOUR
POST /api/v1/campaigns60 campaigns, plus its targets charged to the row quota aboveCAMPAIGN_CREATE_PER_HOUR
POST · PATCH · PUT · DELETE /api/v1/integrations/connections600 writesINTEGRATION_WRITES_PER_HOUR
POST /api/v1/integrations/connections/{id}/test, and the verification a create/update runs600 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/v1me, 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

CapValueBehaviour at the cap
Targets in one POST …/campaigns/{id}/targets1000Rejected400, page the rest in
Targets in POST /api/v1/campaigns (create + load)1000Rejected400, create then page in
DNC bulk delete (ids)10000 per requestRejected400
DNC CSV export500000 rowsTruncated
Request body2 MB unless the endpoint documents otherwise (uploads are higher)Rejected413

Page sizes are clamped, never rejected — an over-large limit silently returns the maximum:

List endpointDefault limitMax
GET /api/v1/agents100200
GET /api/v1/calls50200
GET /api/v1/campaigns/{id}/results5001000
Dashboard list endpointscommonly 50100commonly 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

LimitValueAt the cap
Widget session-init (/api/public/widget/{slug}/session)10 starts / 60 s, per slug + IP400 "Too many attempts — please wait a moment and try again."
Failed sign-ins, per account10 / 15 min429 — counts failures only; a successful sign-in clears it
Failed sign-ins, per IP50 / 15 min429 — catches one password sprayed across many accounts
Client token TTLdefault 600 s, range 30 s3600 sClamped
Client token maxCallsceiling 100Clamped

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 offset paging silently skips rows that landed between two polls — store nextSince from 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/agents and /api/v1/numbers exist to fill dropdowns — fetch once per run, not once per row.
  • Retry safely. Send an Idempotency-Key on 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 429 rather 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.