Error codes
Every non-2xx response uses the same envelope:
{
"error": {
"code": "INVALID_RECIPIENT",
"message": "Not a valid phone number in international format",
"details": { "recipient": "12345" },
"requestId": "req_3f2c9a..."
}
}
- Branch on
code, not onmessage(messages may be reworded). Codes are stable within/v1; new codes may be added. requestIdis also returned in theX-Request-Idresponse header. Include it when contacting support. You can send your ownX-Request-Id(8–100 chars[A-Za-z0-9._-]) to correlate logs.detailsis code-specific (forVALIDATION_ERRORit is a list of{ path, message, code }).
| HTTP | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | VALIDATION_ERROR | Request body, query or path is invalid | No — fix the request |
| 400 | IDEMPOTENCY_KEY_REQUIRED | POST /v1/messages without Idempotency-Key | No |
| 400 | INVALID_RECIPIENT | Recipient is not a valid phone number | No |
| 400 | WEBHOOK_URL_REJECTED | URL not allowed (not HTTPS, private address, unresolvable) | No |
| 400 | SEGMENT_TOO_COMPLEX | A segment rule took too long to evaluate | No — simplify the conditions |
| 401 | UNAUTHENTICATED | Missing, invalid, revoked or expired API key | No |
| 403 | INSUFFICIENT_SCOPE | API key lacks a scope (details.required) | No |
| 403 | FORBIDDEN | Not allowed (role, or inactive organization/project) | No |
| 403 | CSRF_REJECTED | Dashboard request from an untrusted origin | No |
| 403 | INSTANCE_LIMIT_REACHED | Plan's instance limit reached | After upgrading / deleting |
| 403 | QUOTA_EXCEEDED | Plan quota reached (monthly messages, projects, webhooks) | After the period / upgrade |
| 403 | FEATURE_NOT_AVAILABLE | The plan or project does not include the feature (details.feature: inbox, campaigns) | After upgrading / enabling |
| 403 | CONTACT_LIMIT_REACHED | Plan's contacts-per-project limit reached | After upgrading / erasing contacts |
| 403 | CAMPAIGN_LIMIT_REACHED | Plan's limit of campaigns scheduled, running or paused at once | After one finishes |
| 404 | NOT_FOUND | Resource does not exist or belongs to another project | No |
| 409 | CONFLICT | Generic conflict (e.g. slug taken) | No |
| 409 | EXTERNAL_REFERENCE_TAKEN | Another instance in the project has this externalReference | No |
| 409 | IDEMPOTENCY_KEY_REUSED | Same key used with a different request | No — use a new key |
| 409 | IDEMPOTENCY_REQUEST_IN_PROGRESS | Same key still being processed | Yes, shortly |
| 409 | INSTANCE_STATE_CONFLICT | Operation not possible in the instance's state (e.g. send to LOGGED_OUT) | After fixing state |
| 409 | QR_NOT_AVAILABLE | No QR / pairing code pending (details.status) | Yes, after connect |
| 409 | MEDIA_NOT_READY | Media expired or about to expire | Re-upload |
| 409 | CAMPAIGN_STATE_CONFLICT | Action not possible in the campaign's status (e.g. edit a running campaign) | No |
| 409 | CONTACT_OPTED_OUT | The contact opted out; an opt-in cannot be recorded for them | No — they can send START |
| 413 | PAYLOAD_TOO_LARGE | Body or file too large | No |
| 415 | UNSUPPORTED_MEDIA_TYPE | File type not allowed or content does not match it | No |
| 429 | RATE_LIMITED | Too many requests (Retry-After header, details.retryAfterSeconds) | Yes, after the delay |
| 429 | BACKLOG_LIMIT_REACHED | Too many queued messages in the project | Yes, after ~30 s |
| 500 | INTERNAL_ERROR | Unexpected error (logged with the request ID) | Yes, with backoff and the same idempotency key |
| 503 | SERVICE_UNAVAILABLE | Temporarily unavailable | Yes, with backoff |
A resource that exists in another tenant always returns 404, never 403, so IDs cannot be probed.