Guides
Rate limits
Two kinds of limit apply: a request budget on every API key, and the capacity each feature is provisioned for. Both are defaults, and both can be raised.
Request limits
Every endpoint shares one budget, counted per key rather than per organization, so splitting an integration across two keys genuinely doubles what it can spend.
- 60 / minute
- The burst allowance. Comfortable for interactive use and for a job working through a few thousand records; deliberately short of crawling an entire account in one pass.Counted in fixed one-minute windows.
- 10,000 / day
- The daily ceiling. Polling is what usually spends it: the same integration driven by webhooks costs a handful of requests a day.Resets at midnight UTC.
A few endpoints are metered more tightly than the shared budget, because one request can cost far more than another. Where that is the case it is listed under the feature below.
Feature limits
The platform is shared, so each feature is provisioned with capacity as well as a request budget. These are the defaults on a standard plan.
Outbound campaigns
reference- 50
- Concurrent callsAcross every running campaign in the organization.
- 120 calls / minute
- Dial rate
- 250,000
- Contacts per campaign
- 25
- Running campaigns
Call centers
reference- 100
- Concurrent inbound callsPer organization, across every number and queue.
- 500
- Queued callers
- 250
- Signed-in agents
Help desks
reference- 300 / minute
- Inbound messages
- 10,000
- Open conversations
- 120 / minute
- Outbound replies
- 20,000 characters
- Message length
- 50 MB
- Attachment sizeUp to 10 attachments on one message.
- 30
- Tags per conversation
Contacts and imports
reference- 10,000
- Contacts per requestLarger sets go in as a file URL.
- 100 MB
- Import file size
- 5
- Concurrent import jobsFurther imports queue rather than fail.
- 1,000,000
- Contacts per list
Calls and recordings
reference- 60 / minute
- RequestsThe standard per-key budget.
- 20 / minute
- Recording downloads
- 15 minutes
- Recording URL lifetime
Analytics and reports
reference- 20 / minute
- RequestsThese queries read across the whole account, so they are metered more tightly.
- 3
- Concurrent report runs
- 500,000 per export
- Export rows
Events and streaming
reference- 10 per key
- Concurrent streams
- 2 hours
- Stream lifetimeReconnect with the last event id; nothing is lost.
Webhooks
reference- 20 per organization
- Endpoints
- 4
- Delivery attemptsThe first try, then three retries.
- 10 seconds
- Endpoint timeout
Knowledge bases
reference- 10,000
- Documents per corpus
- 25 MB
- Document size
- 60 / minute
- Uploads
Evals
reference- 3
- Concurrent runs
- 500
- Cases per run
These are defaults, not ceilings
Reading your budget
Every response carries the state of both windows, so a well-behaved client never has to guess or wait for a refusal to find out where it stands.
- X-RateLimit-Limit-RPM
- Requests allowed this minute.
- X-RateLimit-Remaining-RPM
- Requests left in it. Slow down as this approaches zero rather than after it arrives.
- X-RateLimit-Reset-RPM
- Unix timestamp at which the minute window refills.
- X-RateLimit-Limit-TPD
- Requests allowed today.
- X-RateLimit-Remaining-TPD
- Requests left today.
- X-RateLimit-Reset-TPD
- Unix timestamp of the next midnight UTC.
- Retry-After
- Seconds to wait. Obey it: it is calculated from the window that actually refused you, and beats any backoff you compute.Sent only on a 429.
Being refused
{"error": {"code": "RATE_LIMIT_EXCEEDED","message": "Rate limit exceeded","retryAfter": 30}}
The wait is also in the body, for clients that find headers awkward. Retrying straight away on a 429 spends the next window before it opens.
async function fetchWithRetry(url, options, maxRetries = 3) {for (let attempt = 0; attempt < maxRetries; attempt++) {const response = await fetch(url, options);if (response.status !== 429) return response;const retryAfter = Number(response.headers.get('Retry-After'));const waitMs = Number.isFinite(retryAfter) && retryAfter > 0? retryAfter * 1000: 2 ** attempt * 1000;await new Promise((resolve) => setTimeout(resolve, waitMs));}throw new Error('Rate limited after ' + maxRetries + ' attempts');}
Spend fewer requests, not more carefully