# Platform API errors

> The one error envelope of the Platform API (/v1), what every error code means and how to fix it, idempotency rules and rate limits.

Every refusal on the messaging endpoints (`/v1/messages`, `/v1/calls`,
`/v1/campaigns`, `/v1/senders`, `/v1/me`, `/v1/templates`, `/v1/agents`,
`/v1/lists`, `/v1/contacts`, `/v1/contact-fields`, `/v1/conversations`,
`/v1/segments`, `/v1/balance`, `/v1/compliance`,
`/v1/webhooks`, `/v1/events`, `/v1/media` and `/v1/recordings`) is the same
envelope, and so is a `/v1` path that does not exist (`404 not_found`). Only
the OpenAI-compatible endpoints, `/v1/responses`, `/v1/chat/completions` and
`/v1/models`, answer in the OpenAI format ([Errors](/openai-api/errors/)).
This page is about the messaging endpoints, and it is the one place that says
what each code means: the guides list only the codes you are most likely to
meet.

```json
{ "error": {
    "type": "invalid_request_error",
    "code": "sender_not_cleared",
    "message": "This number isn't cleared to send texts yet.",
    "param": "from",
    "hint": "Register the number for business texting: in the dashboard open Phone numbers, pick the number, choose Request registration. Carriers usually take 1 to 3 weeks.",
    "docs_url": "https://docs.omniocto.com/platform-api/sms/#get-a-number-cleared-to-text",
    "request_id": "00000000-0000-4000-8000-000000000008",
    "sender_status": "receiving_only" } }
```

- **Branch on `code`.** It is a stable machine word.
- **Show `message`.** It is a sentence a person can read.
- `param` names what the error is about, in `/v1` names: a body field
  (`to`, `template.id`, `audience.contacts.0.phone`), a query parameter
  (`status`), a path segment (`id`) or a header (`idempotency-key`); or
  `null`.
- `hint` says what to do next and `docs_url` where to read more, when we know.
  An `unknown_field` hint names the field you most likely meant at that place
  in the body (``Did you mean `variables`?``, ``Did you mean `agent`?`` inside
  `content`); `unknown_parameter` does the same for a query parameter, and an
  `invalid_number` hint gives the E.164 form (`Did you mean +14155550100?`).
- `request_id` identifies the request. Quote it when you contact us.
- Some codes carry details beside these, in snake_case. Every one is
  declared on [V1Error](/platform-api/reference/#v1error): `allowed`,
  `allowed_country`, `campaign_id`, `campaigns`, `candidates`, `cap`,
  `channel`, `channels`, `closes_at`, `contact`, `current_version`,
  `errors`, `field`, `fields`, `invalid_rows`, `issues`, `message_id`,
  `missing`, `part`, `problems`, `reason`, `received`, `remaining`,
  `resets_at`, `retry_after`, `sender_status`, `short_credits`,
  `skipped_by_reason`, `status`, `template_status`, `used`, `version`. The
  tables below name the ones each code carries.
- A validation failure (`400 invalid_request`) also carries `issues`, one per
  problem: `[{ "path": ["name"], "code": "invalid_type", "message": "`name` is required." }]`.
  `message` is a sentence about the field; branch on `code`.

The envelope's fields, field by field:
[V1Error](/platform-api/reference/#v1error).

## Types

`type` groups codes by what you do about them. It follows the HTTP status:

| status | `type` | what to do |
|---|---|---|
| 400, 405, 413, 415, 422 | `invalid_request_error` | fix the request; retrying it unchanged fails again |
| 401 | `invalid_request_error` (code `invalid_api_key`) | check the key; the `message` says whether it was missing, sent without `Bearer `, or not valid |
| 402 | `billing_error` | add credits or a payment method |
| 403 | `permission_error` | use a key with the right role |
| 404 | `not_found_error` | check the id; another workspace's ids answer `404` too |
| 409 | `conflict_error` | the resource's state does not allow it, or the same key is still running |
| 429 | `rate_limit_exceeded` | wait `Retry-After` seconds, then retry |
| 500, 502, 503 | `server_error` | retry with the same `Idempotency-Key` |

A `500` is `internal_error`: something failed on our side. An unexpected
failure can also answer `500` with `code: null`; treat both the same way.
Retrying with the same `Idempotency-Key` is safe: a text is never sent twice
for one key, and the other keyed requests replay (see [Idempotency](#idempotency)).

A missing or bad key is `401` with `code: "invalid_api_key"`, answered before
the request reaches the messaging routes; its `type` is
`invalid_request_error`, as OpenAI-compatible clients expect, and it carries
`request_id` like every other error. Its `param` is `authorization`, and its
`message` says whether the key was missing, sent without `Bearer `, or not
valid; it never repeats the key. Branch on `code`, not on `type`.

New codes may be added. Treat a code you do not know by its HTTP status and
`type`, and show its `message`. A refusal that carries no code of its own
answers the word for its status: `unauthorized` (401), `payment_required`
(402), `forbidden` (403), `conflict` (409), `unprocessable` (422) or
`unavailable` (503).

## Codes by resource

Each table says what a code means and what to do about it. A code can
appear in more than one table when more than one resource answers it.

### Every request

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_json` | The body is not valid JSON. Send JSON. |
| 400 | `invalid_request` | A field or query parameter fails validation (`param` and `issues` say which), a parameter was sent twice, or an id is of the wrong kind: in a body field (a `sg-…` as `audience.list`), a path (`/v1/campaigns/cl-…`, `param: "id"`) or a query parameter (`?campaign=cl-…`, `param: "campaign"`); `message` names the prefix it takes and `hint` the route that lists them. As `from`, another kind's id is `invalid_from`. Fix the field. |
| 400 | `unknown_field` | The body has a field this route does not take (inside nested objects too). `hint` names the field you most likely meant. |
| 400 | `unknown_parameter` | The query string has a parameter this route does not take. `hint` names the closest one. |
| 400 | `invalid_idempotency_key` | The `Idempotency-Key` breaks [the format](#idempotency). Use 1 to 255 printable ASCII characters. |
| 401 | `invalid_api_key` | The key is not usable, and `param` is `authorization`. The `message` names one of four causes: no key at all; the key sent in `X-API-Key` instead of `Authorization`; an `Authorization` header without `Bearer ` (a bare key, or another scheme such as `Basic`); or a `Bearer` key that is mistyped, revoked or expired (these three answer alike). Send `Authorization: Bearer <your key>`. Its `docs_url` is [Authentication](/getting-started/authentication/). |
| 404 | `not_found` | No such path, or no such resource in this workspace: another workspace's id answers the same. Check the id. |
| 405 | `method_not_allowed` | The path does not take this method. The `Allow` header lists the ones it does. |
| 413 | `payload_too_large` | The body is over the [size limit](/platform-api/overview/#limits). Send less, or a campaign audience as a list. |
| 415 | `unsupported_media_type` | The body was not sent as `application/json`. Set `Content-Type`. |
| 422 | `idempotency_key_reuse` | The same `Idempotency-Key` came with a different request. Use a new key for a new request. |
| 429 | `rate_limited` | Over a [rate limit](#rate-limits). Wait `Retry-After` seconds and retry with the same key. |
| 500 | `internal_error` | Something failed on our side. Retry with the same key. |
| 502 | `upstream_error` | A service we depend on failed. Retry with the same key. |

### Lists

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_cursor` | `after` is not a `next_cursor` of this list read with these filters. Cursors are opaque and belong to the list and filters that answered them; a cursor answered before 2026-10-17 is refused once. Start again without `after`. |
| 400 | `invalid_request` | `limit` is out of range, or a query parameter was sent twice. |
| 400 | `invalid_request` ([messages](/platform-api/sms/#list-texts), [calls](/platform-api/voice/#list-calls)) | `channel` is missing, a `status` is not one the channel has, or a time is not ISO 8601. |
| 400 | `invalid_number` | The `to` filter is not E.164. In a query string write `+` as `%2B`; the `hint` says so when a bare `+` arrived as a space. |
| 400 | `invalid_from` | The `from` filter is not one of your numbers. |
| 404 | `not_found` | The `campaign` filter is not a campaign of this workspace. |

### Contact lists

`/v1/lists` ([Make a list](/platform-api/campaigns/#make-a-list)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `empty_audience_input` | `"contacts": []`. Leave `contacts` out for an empty list, or send at least one row. Nothing is changed. |
| 400 | `invalid_number` | A row's `phone` is not E.164; `param` names the row (`contacts.0.phone`). |
| 404 | `not_found` | `param: "id"`: no such list in this workspace (or it was deleted). `param: "contact"`: nobody in the list has that id or number. |
| 409 | `list_in_use` (with `campaigns`) | A draft, queued, running or paused campaign uses the list, so it was not deleted. `PATCH` the draft to another `audience.list`, delete it, or cancel the campaign. |
| 409 | `ambiguous_contact` (with `candidates`) | More than one contact in the list has that number; nobody was removed. Remove each by its `ct-…` id. |
| 409 | `import_in_progress` | The first request with this `Idempotency-Key` is still running. Retry with the same key after `Retry-After`. |

### Insights

`/v1/insights`, and `insight` on a voice campaign ([Choose what calls find out](/platform-api/insights/)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `insight_not_supported` | `insight` on a voicemail-drop campaign: it has no conversation to read. |
| 404 | `insight_not_found` | `param: "insight"`: no insight with that id in this workspace. `GET /v1/insights` lists them. |
| 409 | `insight_owned_by_call_plan` | The campaign is a Call plan, which keeps its own insight. Change what it asks in the dashboard or with Octo. |
| 409 | `revision_conflict` (with `revision`) | The insight changed since you read it; nothing was changed. Read it again and redo the edit on what it holds now. |
| 409 | `field_in_use` (with `fields`) | The edit renames or removes a field that has open requests, starts workflows or alerts live; nothing was changed. Keep its key, or send `acknowledge_field_changes: true`. |

### Segments

`/v1/segments` ([Build a segment](/platform-api/segments/)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_filter` (with `issues`) | The `filter` has a problem: `param` names the first one (`filter.all.1.value`), the message says what that field takes, and `issues` lists every problem with its path. A field written the dashboard's way (`channelType`, `dnc:sms`) gets a hint with the `/v1` name. Nothing is saved. |
| 403 | `insufficient_role` | Creating, changing or deleting a segment needs a key created by an owner or admin. Reads work with any key. |
| 404 | `not_found` | No live segment with that id in this workspace (a deleted one too). |
| 409 | `segment_in_use` (with `campaigns`) | A queued, running or paused campaign uses the segment, so it was not deleted, or its filter was not changed (a PATCH changes nothing then, `name` included). Wait for it to finish, or cancel it. A draft does not block. |
| 409 | `create_in_progress` | The first `POST` with this `Idempotency-Key` is still running. Retry with the same key after `Retry-After`. |
| 422 | `segment_filter_invalid` | The segment's saved filter can no longer be used, so its contacts cannot be listed. `PATCH` a new `filter`. |
| 503 | `segment_too_slow` | Reading the segment's contacts took too long and was stopped. Narrow the filter (add a `tag` or `channel` condition) or ask for a smaller `limit`; retry after `Retry-After`. |

### Contacts

`/v1/contacts` ([Keep contacts](/platform-api/contacts/)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_request` | `POST` with neither `phone` nor `email`; `email` is not an email address; `attributes` or `tags` are over their limits; `query` is not 3 to 100 characters, or came with `phone` or `email`; or a change would remove the contact's last phone number and email (delete the contact instead). |
| 400 | `invalid_number` | `phone` (in the body, or the `phone` filter) is not E.164. In a query string write `+` as `%2B`. |
| 404 | `not_found` | No such contact in this workspace, or it was deleted. |
| 409 | `contact_exists` (with `contact`) | Another contact already has this phone number (on texts, calls or WhatsApp) or email, so nothing was created or changed. `contact` is its `ct-…` id: read it or `PATCH` it instead. |
| 409 | `create_in_progress` | The first `POST` with this `Idempotency-Key` is still running. Retry with the same key after `Retry-After`. |
| 409 | `stopped_by_person` (with `channels`) | An opt-in over an opt-out the person sent themselves, such as a STOP. Nothing was changed. Only the person can opt back in: on SMS, by texting START. |
| 409 | `no_address` (with `channels`) | An opt-out for a channel the contact has no phone number or email for. Nothing was changed. Give it one first, or leave that channel out. |

### Balance and compliance

`/v1/balance` ([Credits and balance](/platform-api/billing/#credits-and-balance)),
`/v1/usage` ([Usage and spend](/platform-api/billing/#usage-and-spend))
and `/v1/compliance`
([Accept the outbound compliance acknowledgment](/platform-api/campaigns/#accept-the-outbound-compliance-acknowledgment)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_request` | `POST /v1/compliance/accept` without `version`. Send the `current_version` `GET /v1/compliance` returned. |
| 400 | `unknown_field` | The accept body takes `version` only. |
| 403 | `insufficient_role` | Only an owner or admin accepts the acknowledgment: use a key created by one. Nothing is accepted. |
| 409 | `version_mismatch` (with `current_version`) | The text changed since you read it, so nothing was accepted. Read it again with `GET /v1/compliance` and accept its `current_version`. |
| 400 | `too_many_groups` (with `limit`) | `GET /v1/usage` would answer more than 5,000 groups. Narrow `from` / `to`, or group by less (`day,category` instead of `day,label`). |
| 400 | `export_too_large` (with `limit`) | More than 250,000 charges match `GET /v1/usage/export`. Nothing was sent. Narrow `from` / `to` or filter by `category`, and export each part. |
| 429 | `export_in_progress` | Another usage export of the workspace is running. Retry after `Retry-After` seconds. |

### SMS messages

`POST /v1/messages` with `"channel": "sms"` ([Send a text](/platform-api/sms/)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_number` | `to` is not E.164, or not a real number (`param: "to"`). The `hint` suggests the E.164 form when the number is clear; we never send to the suggestion. Nothing is sent or charged. |
| 400 | `invalid_from` | `from` is not a live number of this workspace. Pick one from `GET /v1/senders`. |
| 400 | `from_not_sms_capable` | `from` is yours but has no SMS channel (a voice-only number). Pick a number whose `sms.ready` is `true`. |
| 400 | `no_sender` | The number's SMS channel has no sender set up yet. Finish its SMS setup in the dashboard under Phone numbers. |
| 402 | `insufficient_credits` | The balance cannot cover the send (`short_credits` says by how much). Nothing is sent. Read it with `GET /v1/balance` and add credits in the dashboard. |
| 402 | `billing_not_eligible` | The workspace's billing does not allow outbound sends: there is no card on file, or the subscription is past due or canceled (`reason` says which). Add a card, or bring the subscription up to date, under Settings → Billing. |
| 402 | `workspace_suspended` | The workspace is suspended, usually for an unpaid balance. See Settings → Billing. |
| 402 | `route_blocked` | Your account's sending rules refuse this destination. |
| 409 | `opted_out` | The number opted out (it texted STOP, or is on your do-not-contact list). Nothing was written. Do not resend. |
| 409 | `send_in_progress` | The first request with this `Idempotency-Key` is still being handed to the sending engine. Retry with the same key after `Retry-After` ([Idempotency](#idempotency)). |
| 422 | `sender_not_cleared` | The number is not registered with the carriers yet; `sender_status` says where it is ([Get a number cleared to text](/platform-api/sms/#get-a-number-cleared-to-text)). |
| 422 | `trial_sender_not_allowed` | `from` is the shared trial number, which only places calls to a phone number on your profile or a teammate's ([The trial number](/platform-api/voice/#the-trial-number)). |
| 422 | `mms_unsupported_country` | `media` was sent to a number outside +1. MMS goes to +1 numbers only. |
| 422 | `media_not_owned` | `media` is not a file this workspace uploaded. Upload it with `POST /v1/media` first and send the `media` it answers. |
| 422 | `sms_no_rate` | We have no price for this destination yet, so we cannot send to it. |
| 422 | `empty_message` | Neither `text` nor `media` was sent. Send one. |
| 429 | `rate_limited` | Over the send limit ([Rate limits](#rate-limits)). Retry with the same key after `Retry-After`. |
| 503 | `rate_limiter_unavailable` | The send limiter is briefly unavailable. Retry with the same key after `Retry-After`. |

### WhatsApp messages

`POST /v1/messages` with `"channel": "whatsapp"` ([Send a WhatsApp template](/platform-api/whatsapp/)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_number` | `to` is not E.164 or not a real number, checked before anything else; the `hint` suggests the E.164 form when it can. |
| 400 | `invalid_from` | `from` is not a WhatsApp number of this workspace. |
| 400 | `invalid_request` | The body breaks a rule: a name in several languages without `template.language`, variables that mix names and positions, or a template `id` with another template's `name` or `language`. `param` names the field. |
| 400 | `unknown_field` | A field a WhatsApp message does not take. A `text` field gets a `hint` to use `template`: the API sends templates only. |
| 402 | `billing_not_eligible` | As for [SMS](#sms-messages): no card on file, or a past-due or canceled subscription (`reason`). |
| 402 | `workspace_suspended` | The workspace is suspended. |
| 402 | `route_blocked` | Your sending rules refuse this destination. |
| 404 | `template_not_found` | No template with that id, or that name and language, on this number. `hint` suggests the closest name, or the message lists the languages it exists in. |
| 409 | `opted_out` | The person opted out. The message is stored as blocked (`message_id`). Do not resend. |
| 409 | `send_in_progress` | The first request with this key is still sending. Retry with the same key after `Retry-After`. |
| 422 | `trial_sender_not_allowed` | `from` is the shared trial number, which only places calls. |
| 422 | `template_not_on_sender` | The template belongs to a different WhatsApp number than `from`. Send from the number `GET /v1/templates` lists. |
| 422 | `template_not_approved` | Meta has not approved the template; `status` is its current state. |
| 422 | `template_components_mismatch` | The values do not fit the approved template (a missing variable, a button it does not have). `message` names it; nothing was written. |
| 422 | `channel_unavailable` | The WhatsApp number is deleted or inactive. |
| 422 | `channel_pending_verification` | The WhatsApp number is still being verified. |
| 422 | `number_disconnected` | Meta disconnected this WhatsApp Business App number. Reconnect it in the dashboard. |
| 429 | `workspace_rate_limited` | The workspace's rate for template sends through the API is used up. Nothing was written. |
| 429 | `rate_limited` | The number's own sending budget for this second, minute or hour is full (shared with the inbox). |
| 429 | `daily_limit_reached` | The number's daily template cap is used up (one cap per number, shared with campaigns). |
| 502 | `send_failed` | Meta rejected the send. The message is stored as failed (`message_id`). |
| 503 | `idempotency_unavailable` | We could not check the key just now; nothing was sent. Retry with the same key. |

Every WhatsApp `429` carries `retry_after` in `error` and the same number of
seconds in `Retry-After`.

### Agents

`PATCH /v1/agents/{id}` ([Improve an agent between rounds](/platform-api/voice/#improve-an-agent-between-rounds)). Nothing changes on any of these.

| status | code | means, and what to do |
|---|---|---|
| 400 | `prompt_required` | `prompt` is blank. Send the agent's instructions. |
| 400 | `invalid_request` | A field breaks a rule (`prompt` over 50,000 or `greeting` over 2,000 characters); `param` names it. |
| 400 | `field_not_supported` | `voicemail_message` (or `voicemail`): voicemail is a setting of the voice campaign, not of the agent. |
| 400 | `model_not_allowed` | `model.chat` or `model.voice` (`param`) is not a model this agent can use; `allowed` lists the ones it can. |
| 400 | `voice_not_available` | `voice` is not a voice of this agent's voice engine or one the workspace imported; `allowed` lists them (`id`, `name`, `language`). |
| 400 | `language_not_supported` | `language` is not a language calls support; `allowed` lists them. |
| 403 | `insufficient_role` | The key's owner is not an owner or admin of the workspace. |
| 409 | `version_conflict` | The agent changed since you read it: it is no longer at `expected_version`. The error carries `expected_version` and `current_version`. Read it again, then send your change with the new version. |

A saved change can still carry `warnings` in the answer: `model_replaced` (a retired model was saved as its replacement) and `voice_language_mismatch` (the language changed and the voice speaks another one: send a `voice` of that language).

### Rehearsals

`POST /v1/agents/{id}/test-turns` ([Rehearse an agent](/platform-api/testing/#rehearse-an-agent)). Nothing is run or billed on any of these.

| status | code | means, and what to do |
|---|---|---|
| 400 | `message_required` | A later turn without `message`, or a first turn of an agent with no greeting. Send what the person says. |
| 400 | `session_setting_changed` | `channel`, `mission` or `variables` (`param`) differ from the session's. Leave them out, or start a new session. |
| 400 | `variable_missing` | The mission uses a placeholder (`param: "variables.<name>"`) with no value and no fallback: a campaign would skip that person. Add it to `variables`. |
| 400 | `voice_model_not_rehearsable` | The agent's calls run on a speech-to-speech model. Send `channel: "chat"`. |
| 402 | `workspace_suspended` | The workspace is suspended (`reason`). Add credits or fix the payment method. |
| 404 | `session_not_found` | No session of this agent has that `session_id`, or it expired (an hour after its last request). Start a new one. |
| 409 | `session_busy` | Another request of the session is still running. Wait `Retry-After`, then send the turn again with its `Idempotency-Key`. |
| 409 | `session_full` | The session has 50 caller turns. Start a new one. |
| 409 | `agent_changed` | The agent changed since the session started (`session_version`, `current_version`). Start a new session. |
| 429 | `rate_limited` | Over 60 rehearsal requests a minute for the workspace. |
| 502 | `upstream_error` | The agent's model did not answer; the session is unchanged. Retry with the same key. |
| 503 | `unavailable` | Sessions are unavailable just now. Retry with the same key. |

### Calls

`POST /v1/calls` ([Place a call](/platform-api/voice/)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_number` | `to` is not E.164 or not a real number; nothing was dialed. |
| 400 | `invalid_from` | `from` is not a number of this workspace that can place calls, or the trial number's id is wrong. Copy it again from `GET /v1/senders`. |
| 400 | `invalid_request` | The body breaks a rule (`mission` over 4,000 characters, an `agent` that is another kind of id). `param` names the field. |
| 400 | `agent_required` | No `agent` was sent and the `from` number has no agent of its own (or its agent was deleted); nothing was dialed. Name one from `GET /v1/agents`. |
| 400 | `daily_voice_minutes_exceeded` | The workspace's daily talk-minute cap is used up; the error carries `cap`, `used` and `remaining`. Wait for the reset at midnight in the workspace's timezone, or raise the cap under Settings → Voice. |
| 402 | `route_blocked` | Your calling rules refuse this destination (`allowed_country` names where you can call). On the trial number: `to` is not a phone number on your profile or a teammate's, or no profile has a number yet. |
| 402 | `trial_daily_cap` | The trial number's calls for today are used up; `resets_at` says when it clears. |
| 402 | `trial_lifetime_cap` | The trial number's calls are used up for good. Buy or bring a number. |
| 402 | `trial_calling_off`, `trial_platform_cap` | Trial calling is not available right now. Show `message`. |
| 402 | `workspace_suspended` | The workspace is suspended or cannot pay for calls. See Settings → Billing. |
| 404 | `agent_not_found` | `agent` is not an agent of this workspace, or it was deleted; nothing was dialed. List them with `GET /v1/agents`. |
| 404 | `voicemail_recording_not_found` | `call_settings.voicemail.recording` (on a call or a voice campaign) is not a voicemail recording of this workspace; nothing was dialed or saved. List them with `GET /v1/voicemail-recordings`. |
| 409 | `agent_disabled` | The agent is paused or archived. Enable it first. |
| 409 | `call_in_progress` | The first request with this key is still running. Retry with the same key after `Retry-After`. |
| 409 | `calls_not_routed` | The number was brought from your own carrier for texts only, so its calls do not come to us. |
| 429 | `rate_limited` | More than 10 calls a minute in this workspace. Wait `retry_after` seconds (also the `Retry-After` header). |
| 502 | `dispatch_failed` | The call could not be placed. Try again later. |
| 503 | `trial_check_unavailable` | We could not check a trial call just now. Try again in a moment. |
| 503 | `idempotency_unavailable` | We could not check the key just now; nothing was dialed. Retry with the same key. |

`/v1/voicemail-recordings` ([Voicemail recordings](/platform-api/voice/#voicemail-recordings)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `no_file` | No `file` part was sent (or the body was cut short). Send the audio as `-F file=@message.mp3`. |
| 413 | `audio_too_large` | The audio is over 5 MB. |
| 413 | `storage_quota_exceeded` | The workspace's file storage is full. Delete recordings or files you no longer use. |
| 415 | `unsupported_audio_format` | Not an MP3, M4A or OGG file (WAV is not accepted). |
| 409 | `voicemail_recording_in_use` | A draft, queued, running or paused campaign plays this recording (`campaigns` names them). Point those campaigns at another recording, or wait until they finish. |

`POST /v1/calls/{id}/end` and `GET /v1/calls/{id}/voicemail`
([End a call or get its voicemail](/platform-api/inbox/#end-a-call-or-get-its-voicemail)).

| status | code | means, and what to do |
|---|---|---|
| 404 | `no_voicemail` | The call has no voicemail: it never reached one, or nobody left a message. |
| 502 | `hangup_failed` | The hangup could not be sent, so the call may still be on. Try again; ending twice is safe. |

### Conversations

`/v1/conversations` ([Work the inbox](/platform-api/inbox/)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_request` | A filter word that is not one (`status`, `channel`, `include_campaigns`), a reply with neither `text`, `media` nor `template`, a `template` sent with `text` or `media`, or a search `query` that is empty or over 200 characters. `param` names it. |
| 404 | `not_found` | No such conversation in this workspace (or contact, for the `contact` filter). |
| 404 | `template_not_found` | No approved template by that id or name on the conversation's number. |
| 409 | `opted_out` | The person opted out of this channel, so the reply was not sent. The thread keeps it, marked not sent. Do not resend. |
| 409 | `conversation_closed` | A closed conversation reopens as `active` only. Send that first. |
| 409 | `call_in_progress` | `PATCH` cannot close a call still on the line. End it with `POST /v1/calls/{id}/end`. |
| 409 | `status_changed` | The status changed while your `PATCH` ran (the AI agent handed it off, a teammate closed it), so nothing was changed. Read it again and decide. |
| 409 | `reply_in_progress` | The first reply with this `Idempotency-Key` is still running. Retry with the same key after `Retry-After`. |
| 422 | `reply_window_closed` (with `closes_at`) | Meta's 24 hours since the person last wrote are over. On WhatsApp send a `template`; elsewhere wait for them to write. |
| 422 | `channel_not_supported` | A call or an email thread takes no reply here. |
| 422 | `no_delivery_route` | The conversation's channel can no longer send. Reconnect it in the dashboard. |
| 422 | `media_not_owned` | `media` is not a file this workspace uploaded. |
| 422 | `template_not_supported` | A `template` reply on a conversation that is not on WhatsApp. Send `text` instead. |
| 402 | `insufficient_credits` | An SMS reply the balance cannot cover (`short_credits` says by how much). Nothing is sent. |
| 422 | `sms_no_rate` | An SMS reply to a destination we have no price for yet. |
| 429 | `rate_limited`, `daily_limit_reached` | On SMS, this key's send rate, shared with `POST /v1/messages`; on WhatsApp, the number's sending budget. Nothing was sent. Retry with the same key after `Retry-After`. |
| 502 | `dispatch_failed` | The channel refused the reply. The thread keeps it, marked not sent. |

### Events, media and recordings

`GET /v1/events` ([The event stream](/platform-api/events/#the-event-stream)),
`GET /v1/media/{id}` and `GET /v1/recordings/{conversation_id}`.

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_request` | `types` names an event type that does not exist, `api_version` is not `2026-10-01` or `v2`, a parameter was sent twice, or the path id is malformed or of another kind (`param` says which). |
| 400 | `invalid_cursor` | `after` is not a whole number of 0 or more. Pass the `id:` of the last frame you handled, or leave it out. |
| 400 | `unknown_parameter` | The stream takes `after`, `types` and `api_version`; media and recordings take none. |
| 403 | `insufficient_role` | The stream is for owners and admins only. On an open stream this comes as an `error` frame, and the stream ends. |
| 404 | `not_found` | No such file or conversation in this workspace. |
| 404 | `no_recording` | The call has no recording: it did not connect, or recording was off for the agent. |
| 404 | `recording_failed` | Recording was attempted and failed; `message` says why. Do not retry. |
| 429 | `too_many_streams` (with `retry_after`) | This key (5) or workspace (20) has the most streams it may hold open. Close one. |
| 502 | `upstream_error` | The download link could not be made. Retry. |

### Uploads

`POST /v1/media` ([Send a picture (MMS)](/platform-api/sms/#send-a-picture-mms)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `no_file` | No `file` part, or the multipart body could not be read. Send the file as a part named `file`. |
| 400 | `unknown_field` | A form field other than `file` (`param` names it). |
| 400 | `invalid_request` | More than one `file`. Upload each file on its own. |
| 413 | `image_too_large` | The image is over 5 MB. |
| 413 | `video_too_large` | The video, or the request, is over 50 MB. |
| 413 | `image_too_large_prepared` | The image is still too large once prepared. Send a smaller one. |
| 413 | `storage_quota_exceeded` | The workspace's storage is full. |
| 415 | `unsupported_type` | Not a JPEG, PNG or GIF up to 5 MB, or an MP4, MOV or WebM up to 50 MB. |
| 415 | `unsupported_media_type` | The request is not `multipart/form-data`. |
| 422 | `invalid_image` / `invalid_video` | The file could not be read as the type its bytes claim. |
| 422 | `transcode_timeout` | The video took too long to prepare. Send a shorter one. |
| 429 | `upload_busy` / `transcoder_busy` | Too many uploads, or another video is being prepared. Retry after `Retry-After` (`retry_after`: 5 or 10 seconds). |
| 500 | `upload_failed` | The file could not be stored. Retry. |
| 503 | `image_unavailable` / `transcoder_unavailable` | Files cannot be prepared right now. Retry after `Retry-After`. |
| 504 | `image_timeout` | The image took too long to prepare. Retry. |

### Webhooks

`/v1/webhooks` ([Receive webhooks](/platform-api/events/#manage-endpoints)).

| status | code | means, and what to do |
|---|---|---|
| 400 | `invalid_request` | `url`, `event_types` or `api_version` breaks a rule, a deliveries list's `status` is not one of its values, or an id is of another kind. `param` names the field. |
| 400 | `invalid_cursor` | A deliveries list's `after` is from another endpoint or another `status`. Pass the `next_cursor` of the same list. |
| 403 | `insufficient_role` | The key's owner is not an owner or admin of the workspace. Use such a key. |
| 404 | `not_found` | No such endpoint in this workspace, or no such delivery under it (deliveries are kept for 7 days). |
| 409 | `webhook_endpoint_limit` | The workspace has 500 endpoints. Delete one first. |
| 429 | `webhook_test_rate_limited` | More than 10 test events a minute. Wait `Retry-After` seconds. |

### Campaigns

`/v1/campaigns` and its actions ([Run a campaign](/platform-api/campaigns/)).
When a create that starts is refused after the campaign was created,
`error.campaign_id` names it: it is kept as a draft and nothing is sent.

| status | code | means, and what to do |
|---|---|---|
| 400 | `start_required` | A create without `start` (`param: "start"`). Add `"start": "draft"` to save it for preview, or `"start": "auto"` to create and start it now. Nothing is created. |
| 400 | `invalid_from` | `from` is not a number of this workspace that can send on the campaign's channel (`param: "from"`). |
| 400 | `from_not_sms_capable` | An SMS campaign's `from` (on create, or a draft's new `from` on `PATCH`) is a number of this workspace with no SMS channel. |
| 400 | `invalid_number` | An inline contact's `phone` is not E.164; `param` names the row (`audience.contacts.0.phone`) and `hint` gives the E.164 form. |
| 400 | `invalid_variable` (with `problems`, `part`) | A `variables` value is not one reference or plain text, names a field with a space or another character a field name can't have, is too long, is `{{contact.id}}` on a WhatsApp or AI-call campaign (only SMS can send the contact's id), or is a copy code over 15 characters. Every bad value is in `problems`; `param` is the first by `param`. A value over 200 characters says which part (`part`: `text`, `field` or `fallback`) and its length. On a start or resume: a copy code written longer than 15 characters. |
| 400 | `unknown_variable` (with `fields`, `problems`) | `variables` names something the content has no placeholder for, on create or `PATCH` (stored names included, after the merge). `param` is the first by `param`, `fields` every one, `hint` the closest name or how to remove them; each entry of `problems` has its own `hint` and, when there is one, `suggestion`. A voice campaign's names are checked once it has an agent: the `PATCH` that sets `content.agent` checks the names stored before it. |
| 400 | `invalid_schedule` | `tz` is not an IANA zone; `start_at` or `end_at` has passed, falls in a skipped local hour or cannot be read; `end_at` is not after `start_at`; or a window in `hours` starts and ends at the same time. On a start: `schedule.end_at` has passed, so it would send nothing; PATCH a later one. |
| 400 | `empty_audience_input` (with `errors`, `received`, `invalid_rows`) | The audience has nobody to import: `"contacts": []`, or every row was dropped. Nothing is created, and the `Idempotency-Key` is free again. |
| 400 | `field_not_editable` | An edit names a field fixed at create (`audience.contacts`, `audience.csv`, a started SMS campaign's `from`, a started campaign's audience, …). An SMS draft takes a new `from`. |
| 400 | `field_not_for_channel` | A field of another channel's campaigns (`content.mission` on SMS, `retry` on WhatsApp, `limits.spend_cap_credits` on a voicemail drop); `param` and `fields` name them. |
| 400 | `voicemail_drop_unavailable` | Voicemail-drop campaigns are not available yet. Nothing is created. |
| 400 | `daily_voice_minutes_exceeded` | The campaign does not fit its day's calling minutes. The day is the campaign's start day, in the workspace's timezone (`time_zone`, `day`). `metered` is what that day already billed and `held` what other queued or scheduled campaigns hold that day (`held_by` names them); `per_call_minutes` is how long each call is projected at. Schedule it for another day, split the audience, or raise the cap. |
| 400 | `audience_required` | A clone without `audience`. Send `"unreached"` (only the people it did not reach) or `"same"` (everyone it had). |
| 402 | `insufficient_credits` (with `short_credits`) | The balance does not cover the whole campaign (`GET /v1/balance` shows it). A WhatsApp campaign needs 0.1 credits per recipient. |
| 402 | `workspace_suspended` | The workspace is suspended. See Settings → Billing. |
| 402 | `billing_not_eligible` | A voice campaign: no card on file, or a past-due or canceled subscription (`reason`). See Settings → Billing. |
| 403 | `spend_cap_raise_needs_person` (with `cap`, `dashboard_url`) | An API key raised or removed the spending limit of an AI-call campaign that has started. A key can set one where there is none, or lower one; a person raises it on the campaign's page (`dashboard_url`), then resumes. |
| 404 | `not_found` | No such campaign, preview, contact list or segment in this workspace. A deleted contact list or segment answers the same. |
| 404 | `template_not_found` | An unknown template; `hint` suggests the closest name, or the languages it exists in (`param: "content.template.language"`). |
| 404 | `agent_not_found` | `content.agent` is not an agent of this workspace, or it was deleted. On a `PATCH`, a preview or a start of a voice campaign: the campaign's agent was deleted after it was saved; set another `content.agent` with `PATCH`. A campaign whose agent is deleted while it runs skips its remaining calls as `agent_disabled`. |
| 404 | `agent_not_found` (clone) | The campaign's agent was deleted, so its next round has nobody to talk. Send `content.agent`. |
| 409 | `outbound_compliance_required` | An owner or admin accepts the [outbound compliance acknowledgment](/platform-api/campaigns/#accept-the-outbound-compliance-acknowledgment) first, with `POST /v1/compliance/accept`. Nothing is created. |
| 409 | `preview_required` | An SMS draft starts only from a `ready` preview. Preview it, then start with its `preview_id`. |
| 409 | `preview_stale` | The preview is out of date: the campaign was edited after it, something it checked changed (the sender, the template, the agent, the compliance acknowledgment, the list or segment), a newer preview replaced it (reading that preview shows `preview_expired`), or it failed. A preview `id` made before previews recorded what they checked is stale too. A `PATCH` that changes nothing (every field as stored) does not make a preview stale. Preview again. |
| 409 | `create_in_progress`, `start_in_progress`, `resume_in_progress` | The first request with this key is still running. Retry with the same key. |
| 409 | `campaign_already_started` | A start of a campaign that is no longer a draft, started already or by a racing start. Read it with `GET /v1/campaigns/{id}`. |
| 409 | `campaign_completed`, `campaign_canceled` | A start of a campaign that finished, or was canceled. |
| 409 | `campaign_not_pausable`, `campaign_not_resumable`, `campaign_not_cancellable` (with `status`, `allowed`) | The campaign's state does not allow the action. Canceling a draft is `campaign_not_cancellable`: delete it instead (a cancel's `allowed` never lists `draft`). |
| 409 | `campaign_not_editable` (with `status`) | Only a draft or a paused campaign can be edited. Pause a running one first. |
| 409 | `campaign_not_previewable` (with `status`, `allowed`) | Only a draft or a paused campaign can be previewed (`allowed: ["draft", "paused"]`). |
| 409 | `campaign_not_deletable` (with `status`) | Only a draft, or a campaign canceled before it sent anything, can be deleted. A queued, running or paused campaign: cancel it to stop it. A finished one, or one canceled after it sent to someone, keeps its history. |
| 409 | `campaign_not_finished` (with `status`, `allowed`) | A clone of a campaign that is queued, running or paused: a next round would ring the same people at the same time. Wait for `campaign.completed`, or cancel it first. A draft may be copied with `audience: "same"`. |
| 409 | `round_in_progress` (with `campaign_id`) | Another round of the same campaign family is still a draft or calling: a second would call the same people. Start, finish or delete it first. |
| 409 | `clone_in_progress` | The first clone with this `Idempotency-Key` is still running. Retry with the same key after `Retry-After`. |
| 409 | `pause_in_progress` | An SMS campaign just paused is still finishing its last texts. Preview again after `Retry-After`. |
| 409 | `media_link_unavailable` | Resume: the picture changed while its link was refreshed. The campaign is still paused; try again. |
| 409 | `spend_cap_reached` (with `cap`, `used`, `remaining`, `in_flight`, `next_call`, `dashboard_url`) | Resume of an AI-call campaign whose next call would not fit its spending limit: it would only pause again. It stays paused, nothing changes. A person raises the limit on the campaign's page (`dashboard_url`), then resume it. |
| 413 | `payload_too_large` | The audience is over the size limit. |
| 422 | `trial_sender_not_allowed` | `from` is the shared trial number, which places single calls only. Nothing is created. |
| 422 | `channel_not_supported` | Results and clones are for AI-call campaigns; an SMS or WhatsApp campaign reports in `GET /v1/campaigns/{id}`. |
| 422 | `campaign_not_clonable` | The campaign is a Call plan: its next round is made from its results page in the dashboard. |
| 422 | `no_unreached_recipients` (with `by_status`) | A clone with `audience: "unreached"` and nobody left to call: everyone was reached, skipped, opted out or has no phone number. Nothing is created. Clone with `audience: "same"` to call everyone again. |
| 422 | `no_recipients_left` (with `by_status`) | A clone with `audience: "same"` and nobody it had can be called again: all asked to stop, opted out or have no phone number. Nothing is created. |
| 422 | `sender_not_cleared` (with `sender_status`), `sender_missing`, `sms_no_billing_mode` | The SMS sender cannot text yet ([Get a number cleared to text](/platform-api/sms/#get-a-number-cleared-to-text)). |
| 422 | `sender_provider_unsupported` | Texts and SMS campaigns send from Twilio numbers only for now; a Telnyx or Vonage `from` is refused (a single text answers `400 from_not_sms_capable`). |
| 422 | `template_not_on_sender` | The template belongs to a different WhatsApp number than `from`. |
| 422 | `template_not_approved` (with `template_status`) | Create, `PATCH` or start: Meta has not approved the template. Set an approved one. |
| 422 | `all_opted_out`, `no_recipients` (with `skipped_by_reason`) | Everybody was skipped: all opted out, or each for the reason `skipped_by_reason` names (`same_as_sender`: the number is the campaign's own sending number). A WhatsApp or voice start whose audience reaches nobody answers it too. The campaign is kept as a draft, on every channel and on a create with `start: "auto"` too (`campaign_id` names it; a retry with the same `Idempotency-Key` answers the same refusal). |
| 422 | `unmapped_variable` (with `field`, `fields`, `problems`, `param: "variables"`) | A start, or a create that starts, has placeholders with no source and no fallback: `field` is the first, `fields` every one, `problems` each with its own `hint`. Nothing is created or sent. |
| 422 | `unknown_variable` (with `fields`, `problems`) | A WhatsApp or voice start or resume: a stored `variables` name fills nothing. Remove it with `PATCH` and `"<name>": null`. |
| 422 | `agent_required` | An AI-call campaign has no `content.agent`. |
| 422 | `pricing_unavailable`, `preview_failed`, `mapping_too_long`, `media_not_owned`, `too_many_recipients` | The preview could not be built. `message` says why. |
| 422 | `launch_fields_missing` (with `missing`) | The campaign cannot start until the `/v1` fields in `missing` are set; `hint` names the `PATCH`. |
| 422 | `not_launchable` | The campaign cannot start as it is; `message` says why. |
| 502 | `start_failed`, `campaign_restart_failed`, `upstream_error` | A problem on our side kept it from starting or resuming; nothing changed. After a failed SMS start, preview again (its preview is out of date), then retry. |
| 503 | `idempotency_unavailable` | A WhatsApp or voice start could not check its key; nothing was started. Retry with the same key. |

A preview that runs but cannot start the campaign is not a refusal: it
answers `status: "failed"` with one of these codes in `error`, or with
`preview_expired` for an SMS preview older than 30 minutes or replaced by a
newer one; a start from such a preview is refused `409 preview_stale`
([the preview rules](/platform-api/reference/campaigns/#rules)).

## Idempotency

Moved to its own page: [Retries, idempotency and rate limits](/platform-api/retries/#idempotency).
It covers which routes take an `Idempotency-Key`, what a retry and a replay answer, and the
`*_in_progress` and `idempotency_key_reuse` codes.

## Rate limits

Moved to the same page: [Rate limits](/platform-api/retries/#rate-limits) (the request
and sending limits, `429 rate_limited` and `Retry-After`).

## Request ids

Every `/v1` response carries an `X-Request-Id` header, errors and unknown
paths included, and a messaging endpoint's error repeats it as `request_id`
(a `413 payload_too_large` and a `401` too). Send your own `X-Request-Id` (letters, digits, `-`, `_`,
`=`) and we use it, so your logs and ours share one id.