Esta página está disponible solo en inglés por ahora.
Errors
Every 4xx and 5xx response body uses the same envelope:
{
"error": {
"code": "channel_unavailable",
"message": "Channel is not connected. Re-pair to continue.",
"details": { "channel_id": "..." }
},
"request_id": "req-3f7a1c02"
}Match on code — it's the stable contract. message is human-readable and may change between releases. details is optional and code-specific.
The full code set ships in the Zod contract ERROR_CODES. The table below
is canonical.
Auth / identity
| Code | When you'll see it |
|---|---|
auth_missing | No Authorization header sent. |
auth_invalid | Bearer token doesn't match any active key. |
auth_malformed | Header present but not Bearer …, or token format wrong. |
auth_expired | Key or token is past its expiry. (Revocation is auth_revoked.) |
auth_no_email | Dashboard session token has no verified email. |
auth_revoked | Key existed and has been revoked. |
auth_backend_unavailable | 503, not 401. Your token was valid; our user lookup failed. Retry, and keep the session — do not sign the user out. |
Channel lifecycle
| Code | When you'll see it |
|---|---|
channel_not_found | Channel UUID doesn't exist (or belongs to another customer). |
channel_access_denied | Channel-scoped API key being used against a different channel. |
insufficient_scope | 403. The key's scopes don't cover this route. details carries required and granted. An empty scopes array is unrestricted and never triggers this. |
channel_unavailable | Channel is in a terminal state (logged_out, stopped_by_user, disabled_by_admin, error, failed). |
channel_scoped_key | Key is scoped to a single channel but a multi-channel route was called. |
invalid_lifecycle_transition | Tried to connect an already-connected channel, etc. |
pair_code_unavailable | Channel state doesn't allow generating a pair code right now. |
pair_code_failed | Upstream rejected the pair code request. |
no_qr | QR not yet ready (channel hasn't booted), or no QR for this state. |
not_disabled | Tried to re-enable a channel that wasn't admin-disabled. |
concurrent_modification | Two writes collided on the same row; retry your read-modify-write. |
Plan / quota
| Code | When you'll see it |
|---|---|
plan_channel_limit | Plan's channel cap reached. Upgrade or delete an existing channel. |
rate_limited | A token bucket is exhausted. X-RateLimit-Scope says which (customer, channel or ip). Buckets are per-account and per-channel, not per key. Honour Retry-After. |
worker_at_capacity | No worker has free slots right now — extremely rare; retry. |
trial_expired | 402. Trial ended with no paid plan. Blocks sends, story posts, presence updates and channel creation; reads keep working. |
payment_overdue | 402. Account is past due. Same blocking as above. |
sse_connection_limit | 429. More than 10 concurrent /v1/events streams on the account. |
Webhook + media
| Code | When you'll see it |
|---|---|
webhook_endpoint_not_found | Endpoint UUID doesn't exist. |
media_gone | 410. Token is valid but the bytes aged past retention. Permanent — nothing to retry. |
media_token_expired | 401. The signed media token has expired. |
media_token_tampered | 401. Media token signature does not verify. |
media_token_malformed | 401. Media token is not parseable. |
media_too_large | 413. Media exceeds the 25 MB ceiling. |
media_fetch_failed | 400. We could not fetch your source_url (DNS, non-2xx, or timeout). |
media_fetch_blocked | 400. source_url resolves to a private / loopback / link-local address. |
message_not_found | 404. No message with that id on that channel. |
chat_not_found | 404. No such chat on that channel. |
contact_not_found | 404. No such contact on that channel. |
Generic
| Code | When you'll see it |
|---|---|
invalid_request | Body or query failed Zod validation. details contains the issue list. |
not_found | Generic 404. |
bad_request | Generic 400 where no narrower code fits. |
no_op | The mutation was a no-op (already in the target state). |
other | Catch-all for upstream-provider errors. |
internal_error | Unhandled server-side exception. We're alerted; request_id is the trace key. |
undeliverable_recipient | 400. to is a @lid, has more than 15 digits, is non-numeric, has an unknown suffix, or WhatsApp says the number is not registered. details.to carries the value. |
not_implemented | 501. The endpoint exists but is a declared stub (GET /v1/channels/:id/stories, GET /v1/channels/:id/calls). |
payload_too_large | 413. Request body exceeded the server limit. |
This list is not closed. Match on code with a default branch — new
codes ship without a major version bump, and a handful of routes emit codes
outside the core set documented here.
Best practices
- Always log
request_id. Customer support uses it to find the exact request in our traces. - Match on
code, never onmessage. The messages are tuned for human readability and shift over time. - Treat unknown codes as transient. New codes get added; assume retryable until the docs are updated.
- For 429, honor
Retry-Afterinstead of guessing backoff.