Tonedocs

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: 1756029667

Exceeding 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
agentCallsCalls running a Tone agent
byoCallsCalls 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.

Was this page helpful?

On this page