Tonedocs

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. Never POST /v1/compliance/check, which answers 200 with the verdict in the body
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
price_changedPOST /v1/numbers/purchase with confirmedTotalPaise
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 whose request body has a tone_test_ key name a live resource, or the reverse (a resource in the URL path answers 404)
missing_variablesPOST /v1/calls and campaign dials, for an agent with required input variables
sip_trunking_not_enabled, sip_trunk_not_active, sip_trunk_has_numbersPOST /v1/sip-trunks, DELETE /v1/sip-trunks/{id}, POST /v1/calls on a trunk number
billing_details_requiredStarting a top-up in the dashboard
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, 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.
  • Saving an agent whose voice settings the catalog does not allow: a (ttsModel, ttsVoice) pair that doesn't exist, a tuning option the model does not take, or a language the voice cannot speak. These are judged against the live voice catalog, so they are 422, never a 400, and details.path names the field.

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 in the request body, or a tone_live_ key naming a sandbox one — numberId on POST /v1/calls, for example. This is structural — no request field bridges it. Check which key your process loaded.

A resource of the other environment named in the URL path (/v1/numbers/{id}, /v1/campaigns/{id}, /v1/calls/{id}) answers 404 instead, so a key learns nothing about the other environment.

missing_variables

422. The agent marks one or more input variables required, and the dial supplied no value for them and the agent has no default. details.missing lists the names. Pass them in variables, or give the variable a default on the agent. A campaign excludes the recipient row instead of retrying it — the same row would be missing the value on every attempt.


Authentication and authorization

unauthenticated

401. No credential, or one we couldn't verify. Check the header form: Authorization: Bearer tone_live_....

A dashboard-only route answers an API key this way too ("API keys are not accepted here"): minting keys, starting a top-up, managing secrets and integration connections, and setting your sender classification take a dashboard session.

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. A dashboard-only endpoint reached with an API key is not this code — that is 401 unauthenticated.

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 suppressed. details.checkType says by which list:

  • dnc — 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. Nothing overrides it, not even consent. Inspect it at GET /v1/dnc.
  • dnd_register — for promotional calls only, India's DND register, read through a third-party lookup. An active explicit consent for that number and purpose overrides a register listing; record one with POST /v1/consent.

blocked_opt_out

403. Blocked by the calling window, or an enforcement/profile state. message says which. The consent check never blocks — it passes or warns.

  • 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. On a dial, the a2p check refused the call: it is in block mode, and no auto-dialer pre-declaration is on file with your access provider. File the declaration, then record its reference under Compliance → Auto-dialer pre-declaration in the dashboard. The dlt check only warns.

Outside a dial, asking for a 140- or 160-series number without an approved DLT Principal Entity ID returns this code too. Complete DLT verification in the dashboard first.


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. For POST /v1/calls, a live call needs at least ₹12 in the wallet, and none is placed while the wallet is on hold for a payment dispute; details carries balancePaise and minBalancePaise for the balance case. 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.

billing_details_required

422. A top-up was started before your organization saved the billing name, address and state its GST tax invoice will carry. Every top-up gets a GST invoice, and GST rules require those details on it, so the payment is not taken until they exist. Add them in the dashboard (the top-up screen asks the first time, and Settings → Billing & tax edits them later), then top up again.


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.

price_changed

409. You sent confirmedTotalPaise, and the carrier's price for the number has moved since your customer reviewed it. Nothing was reserved or charged. details carries the current totalPaise, setupPaise, rentalPaise and renewsAt — show them, and confirm again with the new total.

number_request_pending

409. You asked for a 140- or 160-series number while your organization's earlier request for the same series is still waiting for review. Refused so a queue of identical requests cannot form — staff answer the first one, and the Numbers page shows its state meanwhile. A request for the other series is accepted. Once a request is answered, fulfilled or declined, you can send a new one.

verification_code_invalid

422. The code you entered for a transfer destination does not match the one the verification call spoke. details.attemptsLeft says how many more tries that code allows; after five wrong codes it stops working and you need a new call (POST /v1/transfer-destinations/{id}/verify).

verification_code_expired

422. No verification code is waiting for this transfer destination: no call has been placed, the code expired (it lasts 15 minutes), or five wrong attempts used it up. Place a new verification call with POST /v1/transfer-destinations/{id}/verify and enter the code it speaks.

call_not_live

409. You asked to transfer a call nobody is talking on: it has ended, it has not been answered yet, or no agent is holding it — it is bridged to your own endpoint, or it was already handed to a person. Only a call an agent is on right now can be transferred.

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.


SIP trunking

sip_trunking_not_enabled

403. Your organization cannot create SIP trunks yet. A trunk is your own carrier terminating on Tone's gateway, which is a contract rather than a toggle: it needs completed business verification AND a grant from our team. Ask for the grant with POST /v1/sip-trunks/access-request; GET /v1/sip-trunks/entitlement tells you where the request stands and which of the two gates is still missing.

sip_trunk_not_active

422. The trunk exists but is not active — either it is still pending_verification (our team has not yet approved it) or it has been disabled (the response's details.disabledReason says why). No call can be placed or answered over it until it is active.

It is also returned, with details.reason: "no_outbound_host", when you dial from a number whose trunk is inbound-only: the trunk has no outbound host, so it cannot place calls. Add one with PATCH /v1/sip-trunks/{id}. Both cases are refused before any compliance check runs.

sip_trunk_has_numbers

409. You tried to delete a trunk that still carries imported numbers. Release those numbers first (DELETE /v1/numbers/{id}), then delete the trunk.

sip_trunking_request_pending

409. You called POST /v1/sip-trunks/access-request while one of your requests is already waiting for review. Refused so a queue of identical rows cannot form for one organization — staff answer the first one, and GET /v1/sip-trunks/entitlement reports its state meanwhile. A rejected request is the one you can resubmit; read its reason from that endpoint first, because resubmitting unchanged wastes both sides.

sip_trunking_already_enabled

409. You asked for SIP trunking on an organization that already has it — there is nothing left to grant. In practice this means the screen you submitted from is holding a stale entitlement: re-read GET /v1/sip-trunks/entitlement, and go straight to POST /v1/sip-trunks.


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