Errors and status codes
The error envelope, every error_code the API returns, and which failures are worth retrying.
The envelope
Every error returns JSON with a human message and a stable machine-readable code, sometimes with a hint:
{
"error": "No contact with this phone in the workspace",
"error_code": "contact_not_found",
"help": "Create the contact first with POST /v1/contacts"
}Rate-limit errors add timing:
{
"error": "Rate limit exceeded",
"error_code": "rate_limited",
"retryAfter": 12,
"resetAt": "2026-08-31T09:20:00Z"
}Branch on error_code, never on the message text.
Retry rules
| Status | Retry? |
|---|---|
400, 401, 403, 404, 422 | No. The same request fails again |
409 | Usually no; the work is done or in flight |
429 | Yes, after retryAfter seconds |
5xx, 502 upstream_unavailable | Yes, with exponential backoff |
Always send an Idempotency-Key on POST /v1/messages before adding retries; then a retry after a timeout can never double-send.
Code reference
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A required field is missing or malformed |
| 400 | effect_requires_instant | message_effect on a drip send |
| 400 | group_requires_instant | Group target on a drip send |
| 401 | invalid_api_key | Key missing, malformed, revoked, or wrong prefix |
| 403 | device_not_owned | The device_id is not a line in this key's workspace |
| 403 | sandbox_key_not_supported_here | Test key on conversation history, the one read the simulator does not cover |
| 404 | not_found | Unknown endpoint or resource outside your scope |
| 404 | message_not_found | No queue item or message with that id in your scope |
| 404 | contact_not_found | No contact with that phone; opt-out and bot writes never auto-create |
| 404 | group_not_found | group_chat_id not in this key's workspace |
| 409 | no_online_device | Instant send with no eligible online line |
| 409 | idempotency_conflict | Same Idempotency-Key still in flight |
| 422 | not_deliverable | Understood but cannot be delivered as asked |
| 422 | audio_url_unreachable | Voice memo URL did not fetch as audio |
| 422 | invalid_status | Bad enum value on a state change |
| 400 | resubscribe_confirmation_required | opted_out: false without confirm_resubscribe: true |
| 400 | always_on_confirmation_required | status: active_always without confirm_always_on: true |
| 429 | rate_limited | Over an API rate limit; wait retryAfter |
| 502 | upstream_unavailable | Transient infrastructure failure; retry shortly |
New codes can appear; treat an unrecognised code on a 4xx as non-retryable and on a 5xx as retryable.
The two confirmations, and why they exist
Two writes are deliberately harder than the rest. Re-subscribing an opted-out contact requires confirm_resubscribe: true, and switching the AI assistant to 24/7 replies requires confirm_always_on: true. Both actions can reach a person who asked not to be reached, or at 3am; the extra flag makes them impossible to trigger by accident from a generic client.

