Error catalog
Every error code this API can return, what causes it, and what to do about it.
Every error has the same shape:
{
"error": {
"type": "compliance",
"code": "blocked_dnd",
"message": "That number is on your Do-Not-Call list.",
"details": { "checkId": "8814" },
"doc_url": "https://docs.usetone.ai/errors#blocked_dnd",
"request_id": "req_8f14a2c9..."
}
}Branch on code, never on message. Codes are part of the contract — we add them, we don't
rename them. Messages are written for humans and get reworded.
type is the coarse category (invalid_request, authentication, authorization, not_found,
conflict, rate_limit, compliance, payment_required, server_error) — useful for a
catch-all branch when you meet a code you don't know. details is present when there's structured
context worth having. doc_url links to this page, at the anchor for that code. request_id is
what to quote if you contact us.
Codes will be added over time. Handle an unrecognised one by falling back on type and the HTTP
status.
Retrying
| Retry? | |
|---|---|
429 rate_limited | Yes — after Retry-After |
429 concurrent_call_limit_reached | Yes — but only once calls have ended |
409 idempotency_key_in_use | Yes — after a short delay |
503 provider_unavailable | Yes — with backoff, and keep your idempotency key |
5xx internal_error | Yes — with backoff |
Everything else (4xx) | No. Fix the request. |
provider_unavailable is the one that deserves care: it means the carrier's answer left the
outcome genuinely unknown. Retry with the same Idempotency-Key — that's precisely the case
idempotency exists for. A fresh key risks buying two numbers.
What details carries
Most errors omit it. Where it is present it is structured, and it is the part worth logging:
| Code | details |
|---|---|
validation_error | { "issues": [...] } — one entry per field that failed, with the path and the reason |
blocked_dnd, blocked_opt_out, blocked_dlt_invalid | { "checkType": "dnc", "reason": "..." } — which of the five gate checks refused, and why |
rate_limited | { "retry_after_seconds": 41 } — the same figure as the Retry-After header |
{
"error": {
"type": "compliance",
"code": "blocked_dnd",
"message": "That number is on your Do-Not-Call list.",
"details": { "checkType": "dnc", "reason": "opt_out recorded 2026-08-20" },
"doc_url": "https://docs.usetone.ai/errors#blocked_dnd",
"request_id": "req_8f14a2c9..."
}
}A blocked dial has already written its audit rows by the time you see this,
so GET /v1/compliance/evidence?e164=... explains it in full after the fact.
Where each code comes from
Errors that can appear on any endpoint — validation_error, unauthenticated,
forbidden, insufficient_scope, rate_limited, not_found, internal_error
— are omitted here. These are the ones tied to a particular thing you were doing:
| Code | Raised by |
|---|---|
blocked_dnd, blocked_opt_out, blocked_dlt_invalid | POST /v1/calls, campaign dialling, POST /v1/compliance/check |
concurrent_call_limit_reached | POST /v1/calls, campaign dialling |
insufficient_funds | POST /v1/calls, POST /v1/numbers/purchase, rental renewal, POST /v1/compliance/check |
compliance_incomplete | POST /v1/numbers/purchase |
number_unavailable, provider_* | POST /v1/numbers/purchase, GET /v1/numbers/available |
knowledge_base_not_ready | Dialling an agent whose knowledge base has never built |
document_too_large | POST /v1/knowledge-bases/{id}/documents/upload-url |
idempotency_key_in_use, idempotency_key_reused_with_different_params | POST /v1/numbers/purchase, POST /v1/campaigns/{id}/launch, POST /v1/calls |
environment_mismatch | Any route where a tone_test_ key names a live resource, or the reverse |
email_*, *_token, mfa_required | Sign-up and sign-in only. Not reachable with an API key |
Request problems
bad_request
400. The request was malformed in a way that isn't a field-level validation failure — a missing
required header, for example. POST /v1/numbers/purchase and campaign launch return this when
Idempotency-Key is absent; the message names the header.
validation_error
400. A field failed validation. details names the fields and what was wrong with each.
Common causes: a phone number that isn't E.164 (+919876543210, no spaces), a timestamp that isn't
ISO 8601, a (ttsModel, ttsVoice) pair that doesn't exist, or a misspelled query parameter —
query strings are strict, so agentId where agent_id was meant is a 400 rather than a silently
ignored filter.
unprocessable_entity
422. The request was well-formed but doesn't make sense against current state. The message is
specific and worth surfacing. Frequent cases:
- Naming an
agentIdon a BYO number, or omitting one on a Tone-agent number — the number's routing mode decides which shape is right. - Dialling a number that isn't
active. - Dialling a BYO number with no media endpoint configured.
- Placing a BYO call before setting your organization's sender classification.
- Dialling on a SIP-routed number, which your own platform dials through directly.
not_found
404. No such resource — or it belongs to another organization, which we report identically on
purpose. Also returned for an event older than the 30-day retention window.
conflict
409. The request collided with the world. Most often a number that someone else bought between
your search and your purchase.
idempotency_key_in_use
409. Another request with this Idempotency-Key is still running. Wait and retry; you'll get the
first request's response. See Idempotency.
idempotency_key_reused_with_different_params
422. This key was already used with a different request body. We refuse rather than replaying the
first body under a new request's name. Use a fresh key for a genuinely different operation.
environment_mismatch
422. A tone_test_ key naming a live resource, or a tone_live_ key naming a sandbox one. This
is structural — no request field bridges it. Check which key your process loaded.
Authentication and authorization
unauthenticated
401. No credential, or one we couldn't verify. Check the header form:
Authorization: Bearer tone_live_....
invalid_credentials
401. The credential was read but rejected — a revoked or expired key, or a wrong password on the
dashboard sign-in path.
forbidden
403. Authenticated, but not allowed. Usually a session-only endpoint reached with an API key:
minting keys, starting a top-up, and managing integration connections are deliberately dashboard-only.
insufficient_scope
403. Your key's scopes don't cover this operation. Buying or releasing numbers, provisioning SIP
trunks, managing webhook endpoints, and reading signing secrets all need admin; writes need
write. Mint a key with the right scope — you can't widen an existing one.
invalid_signature
401. A signed service-to-service request failed verification, was replayed, or arrived outside
the clock-skew window.
The five codes below belong to the sign-up and sign-in paths and are not reachable with an API
key. They are listed individually because every error envelope carries
doc_url: .../errors#<code>, and a shared heading would give them all the same anchor.
email_not_verified
403. The credential was correct, but the account's email address has not been verified yet. Send
the user back through the OTP step; a fresh code can be requested from the sign-in screen.
email_exists
Sign-up with an address that is already registered. Note that the sign-up endpoint itself answers
generically when email verification is required — it will not tell you whether an address exists,
because that is an account-enumeration oracle. The dashboard therefore pre-checks with
POST /v1/signup/email-available (public, no credential) and surfaces this code from that result
rather than from the sign-up call.
invalid_token
400. A verification, password-reset or invitation token that did not parse, was already consumed,
or does not belong to this account. Tokens are single-use — issue a fresh one.
token_expired
400. The token parsed but is past its validity window. Request a new one and retry.
mfa_required
Reserved — nothing returns this today. It is declared in the registry so clients can branch on it
before multi-factor sign-in ships, which is the same additive-only promise /v1 makes everywhere
else. Handle it as an authentication failure if you meet it.
Compliance
These are the codes an Indian voice product exists to enforce. Treat them as normal outcomes on a
real recipient list, not as exceptions — a 403 here is the system working.
Every refusal writes an audit row, so GET /v1/compliance/checks explains any block after the fact,
and GET /v1/compliance/evidence?e164=... assembles the full record for one number.
blocked_dnd
403. The recipient is on your organization's do-not-call list — added manually, imported, or
written automatically when someone opted out on a call. Suppression from an opt-out lasts 90 days.
Note that carrier DND data is advisory in this gate: it flags a call, it never blocks one. So this
code always means your own list. Inspect it at GET /v1/dnc.
blocked_opt_out
403. Blocked by consent, calling window, or an enforcement/profile state. message says which:
- No consent on record — promotional calls need consent that hasn't expired. Explicit transactional consent is capped at 7 days by TCCCPR 2025; inferred consent must carry its own expiry.
- Outside the permitted calling window — Indian rules restrict when promotional and collections calls may be placed. This has no override, deliberately. Schedule the call instead.
blocked_dlt_invalid
403. Your DLT registration isn't approved yet. Required before promotional traffic can go out;
complete it in the dashboard. DLT approval is a regulator-side process with no API — it takes as
long as it takes.
Money
insufficient_funds
402. The wallet can't cover this. For a number purchase, that's the setup fee plus the first
month's rental. Campaigns pause themselves rather than fail when the balance gets low — subscribe
to wallet.balance.low and you'll hear about it first.
Top-ups are a dashboard action: funding is deliberately human, so an automated loop can't drain a card.
Limits
rate_limited
429. Too many requests this minute for your organization. Honour Retry-After;
details.retry_after_seconds carries the same number. GET /v1/limits shows your current budget.
concurrent_call_limit_reached
429. Too many calls in flight at once — a different problem from rate_limited, which is why it
has its own code. Slowing down your requests won't help; you need calls to end, a lower campaign
maxConcurrent, or a raised quota. Agent calls and BYO calls draw from separate pools, both visible
at GET /v1/limits.
Carrier and provisioning
number_unavailable
409. Someone bought that number between your search and your purchase. Search again and pick
another — carrier inventory is genuinely first-come.
provider_unavailable
503. The carrier couldn't be reached, or answered in a way that leaves the outcome unknown.
This is not "it failed" — it's "we don't know". Retry with the same Idempotency-Key.
provider_quota_exhausted
429. The carrier's own daily provisioning cap is spent. Not something you can retry your way out
of today; try tomorrow, or contact us if you need a larger allocation.
provider_misconfigured
500. Our carrier credentials are wrong. This one is on us, it's alarmed on our side, and no
change to your request will fix it. Please do get in touch with the request_id.
compliance_incomplete
403. Buying a live number requires completed GST/CIN details and signatory KYC. Finish the
verification wizard under Telephony → Compliance in the dashboard. Test mode needs none of
this — build against it while the paperwork is in flight.
Knowledge bases
knowledge_base_not_ready
409. The base is attached to an agent but no build has succeeded yet, so there's nothing to answer
from. Builds are asynchronous: wait for knowledge.build.completed, or poll the base until its
status settles.
document_too_large
413. The upload exceeds the per-file cap. Split it.
Ours
internal_error
5xx. Something broke on our side. Retry with backoff. If it persists, send us the request_id —
it identifies the exact request in our logs, which turns a support thread into a lookup.