Tone docs

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_limitedYes — after Retry-After
429 concurrent_call_limit_reachedYes — but only once calls have ended
409 idempotency_key_in_useYes — after a short delay
503 provider_unavailableYes — with backoff, and keep your idempotency key
5xx internal_errorYes — 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:

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

CodeRaised by
blocked_dnd, blocked_opt_out, blocked_dlt_invalidPOST /v1/calls, campaign dialling, POST /v1/compliance/check
concurrent_call_limit_reachedPOST /v1/calls, campaign dialling
insufficient_fundsPOST /v1/calls, POST /v1/numbers/purchase, rental renewal, POST /v1/compliance/check
compliance_incompletePOST /v1/numbers/purchase
number_unavailable, provider_*POST /v1/numbers/purchase, GET /v1/numbers/available
knowledge_base_not_readyDialling an agent whose knowledge base has never built
document_too_largePOST /v1/knowledge-bases/{id}/documents/upload-url
idempotency_key_in_use, idempotency_key_reused_with_different_paramsPOST /v1/numbers/purchase, POST /v1/campaigns/{id}/launch, POST /v1/calls
environment_mismatchAny route where a tone_test_ key names a live resource, or the reverse
email_*, *_token, mfa_requiredSign-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 agentId on 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.

Was this page helpful?

On this page