Errors
Every API v2 error is {code, message, details} at the root. The codes you will meet, what they mean, and the details keys that tell you what to do next.
One shape
JSON{ "code": "invalid_request", "message": "Invalid request body", "details": { "errors": [{ "path": ["to"], "message": "Required" }] } }
There is no {"error": {...}} wrapper anywhere in v2. Branch on code; show
message to a person. A message never carries a stack trace or a source path.
Details keys
Keys that mean the same thing on every error:
| Key | Meaning |
|---|---|
errors | On every 400 invalid_request for a bad body or query: [{path, message}], with path naming the field (["to"], ["contacts", 2, "recipient"]). |
recommendation | One sentence saying what to do next. |
dashboardUrl | The dashboard screen that fixes it. |
connect | On 409 no_channel: what this project can connect to reach the recipient, each with the exact request, or null when it is connected from the dashboard. |
from / address | The address the error is about, under the name the request used. |
Codes
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or wrong. See details.errors. |
| 400 | invalid_json | The body is not JSON. |
| 400 | duplicate_identifier | Another contact already holds this identifier on this channel (details.conflicts). |
| 400 | invalid_bot_token | Telegram rejected the bot token (details.telegramSaid). Nothing was connected. |
| 401 | unauthorized | Missing or invalid API key. |
| 402 | insufficient_balance | The balance cannot cover it. |
| 402 | paid_plan_required | Needs a paid plan, for example buying a number. |
| 402 | spend_cap_reached | The team's monthly spend cap is reached. details carries capCents, spentCents and dashboardUrl. |
| 403 | kyb_required | One-way SMS is closed to this account until its business verification is approved. Not returned while one-way SMS needs no verification, which is the default. details.dashboardUrl is /kyb. |
| 403 | ten_dlc_required | SMS to a +1 number needs an approved 10DLC campaign. details.connect lists the registration calls, details.alternatives the channels that need none. |
| 403 | channel_inactive | The channel you named is connected but switched off. |
| 403 | sending_suspended | Sending from this account is suspended. Contact support. |
| 403 | plan_limit_reached | The plan has no room for another active connection. |
| 404 | not_found | The resource, or the address, is not in this project. |
| 405 | method_not_allowed | Known path, wrong method. details.allowed and the Allow header list the methods. |
| 409 | no_channel | Nothing this call could use can reach the recipient. details.connect says what to connect. |
| 409 | ambiguous_from | Two channel groups share the address. Pick one of details.candidates. |
| 409 | conflict | A repeated idempotencyKey: nothing was sent again, and the original is in details.message (or details.campaign). Also a template name this project already uses. |
| 409 | invalid_status | The campaign is in a state that does not allow this (details.status). |
| 409 | email_domain_missing, email_domain_not_verified, domain_setup_failed | The email address cannot be connected yet. details.recommendation has the fix. |
| 409 | disconnect_unavailable | This transport cannot be removed through the API. The message says what to do instead. |
| 409 | channel_cannot_receive | An agent cannot answer on one-way SMS. |
| 422 | sms_compliance_blocked | The content check refused an SMS, or could not run (the message says which). Nothing was sent or charged. details.requiredFixes and details.suggestedCompliantVersion say what to change. See SMS. |
| 429 | rate_limited | The per-project request limit. Wait for Retry-After. |
| 429 | rate_limit_exceeded | A narrower limit: one SMS per recipient per minute, or too many agent test runs for the project. The message says when to retry. |
| 429 | daily_limit_exceeded | A channel's daily sending ceiling (details.limit). Resets at 00:00 UTC. |
| 503 | verification_unavailable | The DNS check of an email domain could not run. Nothing changed; retry. |
Sending limits, plan allowances and reviews are the same as on v1. See Sending limits.
Retrying a 409
Every 409 is a settled answer: retrying the same request cannot change it.
It carries the header x-should-retry: false, on v1 and v2 alike, and the
SDKs honour it, so they raise the 409 as a ConflictError at once instead
of retrying it. A repeated idempotencyKey is the case to handle on purpose:
the original is in the error's body, and that is how a retry learns the first
attempt landed.
Wrong method
A path under /v2/messages, /v2/conversations, /v2/contacts,
/v2/channels, /v2/templates, /v2/campaigns, /v2/agents, /v2/webhook
or /v2/email-domains called with a method it does not take answers 405, not
404:
JSON{ "code": "method_not_allowed", "message": "PUT is not supported here. This path takes GET, POST.", "details": { "allowed": ["GET", "POST"] } }
An unknown path is 404.