Limits and quotas
Two budgets that fail in different ways, and need different responses.
curl "$TONE_API/v1/limits" -H "Authorization: Bearer $TONE_KEY"{
"data": {
"environment": "live",
"requests": { "limitPerMinute": 600, "remaining": 594 },
"concurrency": {
"agentCalls": { "inUse": 2, "limit": 10 },
"byoCalls": { "inUse": 0, "limit": 10 }
}
}
}Live and test have separate budgets, and this answers for whichever key you asked with.
Requests per minute
A per-organization token bucket. Every keyed response carries the headers, on
successes too — a client that only reads them on a 429 has already been
throttled:
RateLimit-Policy: "org";q=600;w=60
RateLimit: "org";r=594;t=41
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 594
X-RateLimit-Reset: 1756029667Exceeding it is 429 rate_limited with a Retry-After. Slow down and retry.
Dashboard traffic does not spend this budget, so a busy browser tab cannot throttle your integration.
Concurrent calls
Two pools that do not borrow from each other:
| Pool | |
|---|---|
agentCalls | Calls running a Tone agent |
byoCalls | Calls bridged to your own media stack |
Exceeding one is 429 concurrent_call_limit_reached — a different failure
that slowing down does not fix. You have to wait for calls to end.
A campaign never dials past your quota, whatever its maxConcurrent says. Each
tick it dials at most the agent calls your quota has free, counting every call
already running for your organization (other campaigns and direct dials
included), and the rest of its recipients wait for the next tick. So a
maxConcurrent above your quota is not an error. It just never takes effect.
⚠️ Inbound and outbound currently share the agent pool. A campaign at your quota can lock out your published line.
Dashboard test calls and chats
The test call and the chat tester in the dashboard are free, and have limits of their own:
- A daily allowance per organization, which resets at midnight IST. It is higher once your business verification is complete.
- A daily allowance per person, counted across every organization you belong to that is not yet verified.
- A small number at once. Test calls can never take all the lines, so your live calls always have room.
Reaching an allowance returns 429 rate_limited. The dashboard shows when it
resets. Finishing verification is the way to raise it.
Handling both
if (res.status === 429) {
const { error } = await res.json();
if (error.code === 'rate_limited') {
await sleep(Number(res.headers.get('retry-after')) * 1000);
return retry(); // submitting too fast
}
if (error.code === 'concurrent_call_limit_reached') {
return queueForLater(); // too many calls live — waiting won't help soon
}
}Both are 429, and treating them the same means either hammering a full
concurrency pool or needlessly stalling on a rate limit. Branch on error.code.
Need more? Both are raised per organization — ask.