Errors
Every error uses the same envelope, with an HTTP status and a stable machine-readable code:
{
"error": {
"code": "validation_failed",
"message": "to must be a phone number with 8-15 digits"
}
}
Treat code as the contract and message as human-facing text (it may change).
HTTP statuses
| Status | Meaning | Retry? |
|---|---|---|
400 | Malformed request or validation failure | No — fix the request |
401 | Missing, invalid or revoked credentials | No — fix the key |
403 | Authenticated but not permitted (scope, number, IP) | No — change key restrictions |
404 | Object does not exist in your workspace | No |
409 | Conflict with current state (number paused, template not approved) | No |
429 | Rate limited (reserved — no hard limit is enforced today) | Yes, with backoff |
500 | Our fault | Yes, with backoff, then contact support |
Codes
| Code | Status | Where |
|---|---|---|
idempotency_key_required | 400 | Sending without the header |
validation_failed | 400 | Any invalid field |
invalid_upload | 400 | Contact import file could not be parsed |
unauthorized | 401 | Bad or missing token |
scope_not_permitted | 403 | Key lacks the scope |
ip_not_allowed | 403 | Request from outside the key's allowlist |
number_not_allowed | 403 | Key is not bound to that number |
message_not_found | 404 | Unknown message id in this workspace |
template_not_found | 404 | Unknown template |
number_not_found | 404 | Unknown number |
number_not_active | 409 | Number is paused |
template_not_approved | 409 | Template is not approved |
Retrying safely
- Sending: always reuse the same
Idempotency-Keywhen retrying. A retry returns the original message instead of sending twice. - Reading: retrying is always safe.
- Everything else: retry
429/5xxwith exponential backoff; never retry4xxwithout changing the request.
Request ids
Every response carries X-Request-Id. Quote it in support tickets — the dashboard logbook lets you look up
the exact call by that id.