API reference
Errors
Errors use normal HTTP status codes and one JSON shape. Branch on the code, show the message to a developer, keep the request ID.
Error format
{
"error": {
"code": "CONNECTION_OFFLINE",
"message": "Cannot send message: Connection 'Support line' is in status 'DISCONNECTED'. Must be CONNECTED.",
"request_id": "req_123"
}
}| Field | Type | Description |
|---|---|---|
error | object, required | |
error.code | string, required | |
error.message | string, required | |
error.request_id | string, required | |
error.details | object |
codeis stable. Write your logic against it.messageis for people and can change. Do not parse it.request_ididentifies the request. It is also in theX-Request-IDresponse header. Include it when you ask for help.detailsappears on some errors, for exampleremaining_attemptson a wrong OTP orvalidationErrorson a malformed body.
Error codes
| Code | HTTP | Meaning | Retry? |
|---|---|---|---|
INVALID_REQUEST | 400 | The body is missing a field or a value is malformed. | No. Fix the request. |
INVALID_PHONE | 400 | The phone number is not valid. | No. |
MISSING_TEMPLATE_VARIABLE | 400 | A template variable has no value. | No. |
CONNECTION_OFFLINE | 400 | The WhatsApp number is not connected. | After the number is linked again. |
OTP_INVALID | 400 | Wrong one-time code. | The user can try again. |
OTP_EXPIRED | 400 | The code is past its expiry. | Send a new code. |
OTP_MAX_ATTEMPTS | 400 | Too many wrong tries. | Send a new code. |
UNAUTHORIZED | 401 | The API key is missing, invalid, expired or revoked. | No. |
FORBIDDEN | 403 | The key lacks the scope. details.required_scope names it. | No. |
QUOTA_EXCEEDED | 403 | A monthly plan limit is reached. | After upgrading or next month. |
NOT_FOUND | 404 | The resource does not exist in this workspace. | No. |
IDEMPOTENCY_CONFLICT | 409 | The idempotency key was used with a different body. | No. Use a new key. |
CONFLICT | 409 | A request with the same idempotency key is still running, or an OTP was already verified. | For a running request, shortly after. |
RATE_LIMITED | 429 | Too many requests from this key, or too many codes to one phone. | Yes, after a pause. |
OTP_COOLDOWN_ACTIVE | 429 | A code was sent to this phone moments ago. | After details.retry_after_seconds. |
INTERNAL_SERVER_ERROR | 500 | Something failed on our side. | Yes, with the same idempotency key. |
Retrying safely
Retry on network errors, timeouts, 429 and 5xx, with a growing delay between tries. Do not retry other 4xx responses: the same request will fail the same way. When you retry a send, reuse the same Idempotency-Key so the message cannot go out twice.
A 202 from POST /v1/messages means queued, not delivered. A message that later fails does not produce an API error. You learn about it from the FAILED status or the message.failed webhook.