# Error catalog

Source: https://docs.usetone.ai/errors

> Every error code this API can return, what causes it, and what to do about it.

Every error has the same shape:

```json
{
  "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 |

```json
{
  "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 `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).

### `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.
