Skip to content

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

HeaderMeaning
X-RateLimit-LimitRequests per minute for this key.
X-RateLimit-RemainingRequests left right now (0 on a 429).
X-RateLimit-ResetWhole seconds until the budget is full again.
Retry-AfterOn 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: 60
json
{
  "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-After seconds 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) and POST /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 replayed client_ref is acknowledged without contacting the provider twice. Never generate a fresh client_ref for 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. hangup is exempt and never answers 429 — 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_id or attempt id exists — 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.

codeWhereRetry when
export_in_progressRecording/CDR exportThe in-flight export for your tenant finishes. An identical repeat then takes the reuse path and answers from the archive it produced.
supervision_capacityStarting supervision of a callA supervised call ends. This is the tenant's supervision quota — its own ceiling, never the dialing one.
too_many_connectionsPOST /v1/ws/ticket and the /v1/ws handshakeA 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:

  1. 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.
  2. 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:

WhereBodyCan a browser read it?
POST /v1/ws/ticketerror.scope names the full cap: widget_token or tenant. No ticket is minted.Yes — it is a plain HTTP response.
The GET /v1/ws handshakeThe 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 other 429; 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:

CapWhere you see itWhat it stops
sms_monthly_budget_usdGET /v1/meSMS sends beyond the monthly budget (429 sms_spend_cap_exceeded).
ai_monthly_budget_usdGET /v1/meVoice-AI minutes beyond the monthly budget.
ai_max_call_secondsGET /v1/meAny 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; 100 is 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; under hard_block the 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 an alert_email still 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/alerts as 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 ​

LimitValueBehavior at the limit
Request body size8 MiBRejected. Split large imports into batches.
Webhook body size1 MB413.
Enabled outbound webhook endpoints20 per tenant422 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 100Values 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/import takes arrays and is idempotent) rather than importing one debt per request.
  • Backoff with jitter on every 429 except sms_spend_cap_exceeded, and on every 5xx. 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.

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.