Appearance
Rate limits & caps
Versión en español: Límites de uso y topes.
Dialer Digital brakes in more than one place, and the code of the standard error envelope always names which brake you hit. This page walks through the ones you are most likely to design around; it is not a closed census of every 429 the API can return. A 429 carries a Retry-After header only where the platform knows when capacity returns — the per-key quota below and the fixed-window brakes. Every other 429 omits it on purpose, and your client owns its backoff.
The rule that survives new codes
Retry any 429 with backoff and jitter — except the spend caps, which retrying cannot clear. Branch on that distinction rather than on a list of codes: a client that enumerates today's codes stops backing off correctly the moment a new one ships.
429 rate_limited — the per-key request quota
Shipped in core v0.86.0 (published as v0.86.0-prod-0.1)
The per-key quota and the Retry-After / X-RateLimit-* headers (core#1471, ADR #193) are in the released contract, so they appear in the API Reference.
Every tenant API key (dd_…) has its own request budget on /v1: two keys of the same account never share one, so a runaway integration on one key cannot starve its sibling. It is a backstop against runaway loops, not a plan limit — the default is generous (3000 requests per minute per key, a sustained 50 per second with a one-minute burst), and it is counted only after your key authenticates, so a wrong key spends nothing.
Every response to a counted dd_ request says where you stand (the exempt hangup, and a request admitted because the limiter could not decide, carry no X-RateLimit-* headers):
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests per minute for this key. |
X-RateLimit-Remaining | Requests left right now (0 on a 429). |
X-RateLimit-Reset | Whole seconds until the budget is full again. |
Retry-After | On the 429 only: whole seconds (at least 1) until one request fits. |
http
HTTP/1.1 429 Too Many Requests
retry-after: 1
x-ratelimit-limit: 3000
x-ratelimit-remaining: 0
x-ratelimit-reset: 60json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}- The refused request was not processed, and a refusal does not spend budget: waiting
Retry-Afterseconds is always enough. Retry the same request after that wait. - Not counted:
POST /v1/calls/{call_id}/hangup(refusing a teardown would leave a call alive), dashboard-user (ddu_) and widget (ddw_) sessions (they have their own brakes below), inbound webhooks and the health routes. - Per node. The count lives in the memory of the node that served you, so behind several nodes the effective ceiling can be higher than the figure; do not use it as an exact meter.
- The limit is adjustable per account — ask your account manager. A value that is not a positive number falls back to the default; there is no setting that means "unlimited".
- If the limiter itself cannot decide, the request is admitted rather than refused: the quota bounds load, it is not a security control.
429 throttled — rate brakes
Operations that reach the telephony or SMS pipeline are throttled when they arrive faster than the pipeline will take them:
json
{
"error": {
"code": "throttled",
"message": "dial throttled (rate)"
}
}- Where:
POST /v1/campaigns/{id}/dial(manual/preview dials) andPOST /v1/sms/messages. - Retryable: yes. Back off exponentially with jitter (e.g. 1 s → 2 s → 4 s …, capped) and retry the same request. There is no
Retry-After: the telephony pipeline does not know when it will have room, and an invented number would be worse than none. - For SMS, retries must reuse the same
client_ref— it is the idempotency key, and a replayedclient_refis acknowledged without contacting the provider twice. Never generate a freshclient_reffor a retry.
429 rate_limited — request-rate brakes on agent actions
json
{
"error": {
"code": "rate_limited",
"message": "call control rate limit exceeded; retry later"
}
}Call-control verbs and dispositions carry their own request-rate brake. Unlike throttled, which is about the telephony pipeline's appetite, this one bounds how fast a single principal can act:
- The budget is per most specific durable principal — agent, else user, else tenant — never per tenant when a narrower one exists, so one runaway page cannot throttle its sibling agents. Durable is the load-bearing word: the budget survives re-authentication, so logging back in mints a new token but does not buy a fresh budget.
- The four call-control toggles share one budget.
hangupis exempt and never answers429— refusing a teardown would leave a call alive that its operator asked to end. - Dispositions have their own, narrower budget. What it bounds is durable row growth: every accepted disposition inserts a correction row into the append-only attempt history.
- Refusal happens before the lookup, so the response is identical whether or not the
call_idor attemptidexists — a throttled client cannot use it to probe for what exists.
Retrying in the next window is the correct response, and the 429 says when it opens: Retry-After is the time left in the current window. The same fixed-window brakes guard sign-in (including the second-factor step), widget-token minting, widget SIP credentials, user invitations, password confirmation and platform-admin authentication, and all of them answer with the same header.
Concurrency ceilings — also 429, also retryable
Three more 429s are not rate brakes at all: they say "this resource is busy right now", and each one tells you exactly what has to happen before a retry succeeds.
code | Where | Retry when |
|---|---|---|
export_in_progress | Recording/CDR export | The in-flight export for your tenant finishes. An identical repeat then takes the reuse path and answers from the archive it produced. |
supervision_capacity | Starting supervision of a call | A supervised call ends. This is the tenant's supervision quota — its own ceiling, never the dialing one. |
too_many_connections | POST /v1/ws/ticket and the /v1/ws handshake | A live-floor connection frees up. Bounded per widget token and per tenant — see below for which one is full and what frees it. |
They are covered by the rule at the top: back off and retry. Do not treat them as fatal, and do not reconnect in a tight loop against too_many_connections.
429 too_many_connections — the live-floor socket caps
The live floor (/v1/ws) has two caps, checked in this order:
- Per widget token — only on the agent lane (a
ddw_token): two live sockets by default, one for the agent plus one of overlap while a page reloads. - Per tenant — derived from your active agents, so it grows with the seats you run; a small tenant sits at a fixed floor.
Both caps are checked twice, and the two answers do not carry the same detail:
| Where | Body | Can a browser read it? |
|---|---|---|
POST /v1/ws/ticket | error.scope names the full cap: widget_token or tenant. No ticket is minted. | Yes — it is a plain HTTP response. |
The GET /v1/ws handshake | The same code, no scope. | No. A browser's WebSocket sees a refused handshake as a bare close 1006, indistinguishable from a network drop. |
json
{
"error": {
"code": "too_many_connections",
"message": "live-floor connection limit reached for this widget token",
"scope": "widget_token"
}
}The ticket answer is an early warning, not a reservation: a ticket minted while there was room is still refused if the cap fills before you redeem it — at the handshake (the browser sees 1006), or, in a narrow race, with a socket that opens and closes at once with 1013 (Try Again Later). Branch on the ticket's scope; after a 1006 or a 1013, mint a new ticket with backoff, and its answer tells you whether a cap is the reason.
What to do with each scope:
widget_token— this agent already has its sockets open, almost always in another tab or window of the widget. Close that other session; do not retry in a loop, because nothing on your side frees a slot until one closes. Tell the agent that, rather than showing a generic "reconnecting".tenant— your account's live floor is full. It frees as other sockets close, so back off with jitter as for any other429; if it keeps happening, tell your account administrator — the cap follows your active agents.
In both cases mint a fresh ticket per attempt: a ticket is good for one handshake.
429 sms_spend_cap_exceeded — the monthly SMS budget
json
{
"error": {
"code": "sms_spend_cap_exceeded",
"message": "month-to-date SMS spend is at the tenant cap"
}
}Your account has a month-to-date SMS spend cap (sms_monthly_budget_usd on GET /v1/me). When month-to-date spend reaches it, sends stop before any provider is contacted — no message goes out, nothing is billed.
- Retrying does not help until the cap is raised or the calendar month rolls over. Treat it as a budget event, not a transient error: alert a human.
- Caps are set with your account manager (they live on the admin surface, so a leaked API key cannot raise your own budget).
Why caps exist
Autodialers and SMS blasters are spend amplifiers — a bad loop or a bad list can burn a month's budget in an hour. The caps are a cost-integrity feature: the platform enforces the ceiling upstream of the money, the same fail-closed way it enforces compliance. The caps on your account today:
| Cap | Where you see it | What it stops |
|---|---|---|
sms_monthly_budget_usd | GET /v1/me | SMS sends beyond the monthly budget (429 sms_spend_cap_exceeded). |
ai_monthly_budget_usd | GET /v1/me | Voice-AI minutes beyond the monthly budget. |
ai_max_call_seconds | GET /v1/me | Any single AI call running away. |
Month-to-date usage is queryable at GET /v1/stats/sms-usage and GET /v1/stats/ai-usage — poll them if you want your own alerts before the brake engages.
Spend alerts — who gets them, and when
Separately from those caps, the platform warns you as your spend approaches your plan limit (the balance or credit limit reported by GET /v1/billing/account):
- When. A background check runs every few minutes and records each threshold the first time your spend crosses it: by default at 80 %, 90 % and 100 % of the limit. The thresholds are set with your account manager;
100is always kept, even if removed. They re-arm each time the account is funded again — a top-up on a prepaid plan, a payment that reduces what is owed on a postpaid one. - What is recorded. Every crossing is written once, as it was at that moment (spent, limit, enforcement mode), and listed by
GET /v1/billing/alerts. An 80 % warning stays an 80 % warning even if you top up afterwards. - Who is notified. Only when your plan's enforcement mode is
graduated; underhard_blockthe crossings are recorded but not sent. Where e-mail delivery of alerts is enabled for your deployment, the alert goes by e-mail to the union of the alert address configured on your plan (alert_email) and every active account administrator of your account, without duplicates (addresses are compared ignoring case), up to 20 recipients — the configured address first. The configured address adds a recipient; it never replaces your administrators. An account with no active administrator but with analert_emailstill receives it. - What it does not do. An alert never blocks anything: stopping at the limit is the enforcement mode's job, not the alert's. E-mail delivery is best-effort (one attempt, no retry queue), so treat
GET /v1/billing/alertsas the record, and the e-mail as the heads-up.
The alert address and thresholds live on the admin surface, like the caps: ask your account manager to set or change them.
Request-level limits
| Limit | Value | Behavior at the limit |
|---|---|---|
| Request body size | 8 MiB | Rejected. Split large imports into batches. |
| Webhook body size | 1 MB | 413. |
| Enabled outbound webhook endpoints | 20 per tenant | 422 unprocessable on POST /v1/webhooks or re-enable (PATCH enabled:true); existing ones are kept. See Outbound webhooks. |
Page size (limit param) | max 1000, default 100 | Values above the max are rejected as bad_request. |
Being a good citizen
- Page with cursors (
limit+next_cursor) instead of huge limits. - Prefer the WebSocket live floor over polling for realtime state (agent presence, call lifecycle, pacing).
- Batch lead imports (
POST /v1/debts/importtakes arrays and is idempotent) rather than importing one debt per request. - Backoff with jitter on every
429exceptsms_spend_cap_exceeded, and on every5xx. Branch on the code being a spend cap, not on a list you have to keep in sync — see the rule at the top of this page.