Errors and codes
Errors use conventional HTTP status codes and one JSON shape, with a stable machine-readable code you can branch on.
Error shape
Every error response has a JSON body with a human-readable error and a stable code. Some errors add details: a list of field problems on validation errors, or an object such as { country, allowed }.
400 application/json
{
"error": "Invalid request",
"code": "invalid_request",
"details": [
{ "path": ["to"], "message": "Required", "code": "invalid_type" }
]
}422 application/json
{
"error": "We don't deliver to India yet. This workspace can send to: Nepal.",
"code": "destination_not_supported",
"details": { "country": "IN", "allowed": ["NP"] }
}The error text may change. Write your logic against code and the HTTP status.
Error codes
| HTTP | code | When |
|---|---|---|
| 400 | invalid_request | Validation failed (details lists each problem), bad JSON, both or neither of text and templateId, or text over 6 segments. |
| 400 | invalid_number | The number can't receive SMS — for example a landline. |
| 400 | invalid_variables | A template variable is missing, unknown, empty, over 100 characters or contains a line break. |
| 401 | unauthorized | x-api-key missing or invalid. |
| 402 | insufficient_balance | Workspace balance is lower than the message cost. Top up at your plan rate. |
| 403 | client_disabled | The API client that owns the key is disabled. |
| 403 | workspace_pending | Your workspace has not been activated yet. |
| 403 | workspace_suspended | POST while the workspace is suspended. |
| 403 | forbidden | The message type is not allowed on your plan or workspace (for example promotional outside Enterprise). |
| 403 | template_required | The text doesn't match an approved template of the same type, or the templateId isn't an approved template of this workspace. |
| 404 | not_found | Unknown id, or a message that belongs to another API client. |
| 409 | idempotency_conflict | Idempotency-Key reused with a different body, or the first request is still running. |
| 422 | destination_not_supported | Valid number, but in a country your workspace can't send to yet. details: { country, allowed }. Show "We don't deliver to <country> yet", not "invalid number". |
| 429 | rate_limited | A rate or OTP limit was hit. Wait for Retry-After seconds. |
| 500 | server_error | Unexpected error on our side. Retry with backoff and the same Idempotency-Key. |
| 503 | unavailable | Delivery is temporarily unavailable. Nothing was charged; retry with backoff. |
| 503 | sms_disabled | The SMS service is temporarily switched off. Retry later. |
Key restriction codes
If an account owner sets restrictions on an API client in the dashboard, requests outside them get one of these:
| HTTP | code | When |
|---|---|---|
| 403 | type_not_allowed | The message type is outside the types allowed for this key. |
| 403 | ip_not_allowed | The call came from an IP outside the key's allowlist. |
| 403 | prefix_not_allowed | The number's prefix is outside the key's allowed prefixes. |
| 429 | daily_cap | The key reached its daily message cap. |
| 429 | number_cap | Too many messages to one number in an hour. |
| 429 | suspected_pumping | OTP traffic on this key looks like SMS pumping and the guard is set to block. |
Answers that are not errors
A wrong OTP is a normal answer from POST /sms/otp/verify, returned with HTTP 200:
200 application/json
{ "success": true, "verified": false, "reason": "invalid_code" }| reason | Meaning |
|---|---|
invalid_code | The code does not match. The attempt is counted. |
too_many_attempts | 5 wrong attempts were made. Send a new code. |
no_active_otp | No code was sent for this number and purpose, it expired, or it was already used. |
Likewise, a message that ends as undelivered or failed is reported through its status, with a neutral error code such as delivery_timeout, not as an API error.
Handling errors
| Status | Retry? | What to do |
|---|---|---|
| 400, 403, 404 | No | Fix the request, the key, the template or the workspace setup. |
| 401 | No | Check the key; it may have been rotated. |
| 402 | After top-up | Add balance, then retry with the same Idempotency-Key. |
| 409 | Yes, carefully | If the first request is still running, wait 1 s and retry. Otherwise use a new key for a new message. |
| 422 | No | Tell the user you don't deliver to that country yet; details.allowed lists where you can. |
| 429 | Yes | Wait Retry-After seconds. |
| 500, 503 | Yes | Exponential backoff with jitter, same Idempotency-Key. |