Errors
The error envelope every RaabtaHQ API failure uses, the full list of stable error codes with their HTTP status, and what request_id, reason and details mean.
The envelope
Every failure, from every endpoint, is one shape: an error object with a stable code to branch on and a human message that may be reworded at any time. Successes are { data }; a response with an error key is never a success.
{
"error": {
"code": "validation_error",
"message": "phone: Use E.164, e.g. +971501234567",
"request_id": "req_01J9X4QZ3M8N7K2P",
"details": [
{
"path": "phone",
"message": "Use E.164, e.g. +971501234567"
}
]
}
}| Name | Type | Description |
|---|---|---|
errorrequired | object | What went wrong |
error.coderequired | string | Stable machine-readable code — branch on this |
error.messagerequired | string | Human-readable; may be reworded |
error.request_idoptional | string | Quote this in support conversations |
error.reasonoptional | string | Finer-grained machine string for 422s and 409s |
error.detailsoptional | object[] | Per-field failures |
error.details[].pathrequired | string | Dotted path to the failing field, or the resource name for a limit |
error.details[].messagerequired | string | Human-readable explanation |
Error codes
The full set. The HTTP status is fixed per code — a code and its status can never disagree — and each endpoint's reference page lists the subset it can actually return.
| Status | Code | When |
|---|---|---|
| 400 | bad_request | The request could not be parsed: malformed JSON, an invalid cursor, or a query parameter of the wrong shape. |
| 400 | validation_error | The body or query failed validation. `details` lists each failing field with a `path` and a `message`. |
| 401 | unauthorized | No usable API key: the Authorization header is missing or malformed, or the key is unknown, revoked or expired. The three are deliberately indistinguishable. |
| 402 | plan_limit_reached | The account has reached a plan limit for this resource. `details[0].limit` is the cap; ask the account owner to raise it. |
| 403 | forbidden | The key is valid but lacks the scope this endpoint requires, or the request came from an address outside the key’s IP allowlist. The message says which. |
| 403 | account_suspended | The account this key belongs to is suspended. Rotating the key will not help; contact support. |
| 404 | not_found | No such resource in this account. A resource that exists in another account also returns this. |
| 409 | conflict | The request conflicts with current state: a duplicate phone number on create, an invalid state transition, or an idempotent request that is still in flight. |
| 422 | unprocessable | The request was well-formed but cannot be carried out. `reason` is a stable string saying why (for example `outside_window` or `stage_not_in_pipeline`). |
| 422 | channel_not_connected | The channel this request needs is not connected on the account. Connect it in Settings and retry. |
| 429 | rate_limited | The per-key budget, or the per-IP budget for failed authentication, is exhausted. Honour `Retry-After` before retrying. |
| 500 | internal | Something failed on our side. Safe to retry with the same Idempotency-Key; quote `request_id` if it persists. |
request_id
Every response carries an X-Request-Id header. Send your own and it is echoed back; omit it and one is generated. On a failure the same value is repeated in the body as request_id, so it is available whether you log headers or bodies. Quote it when you contact support — it is the fastest route to the server-side trace.
A replayed idempotent request gets a fresh request id (see Idempotency), so the original and the replay stay distinguishable in your logs and ours.
reason and details
Two codes are too broad to branch on alone, so they carry a second machine-readable field:
| Field | Meaning |
|---|---|
| reason | A short stable string on 422 unprocessable, 409 conflict and 403 forbidden saying why. Examples: outside_window, stage_not_in_pipeline, idempotency_key_reused, idempotency_in_progress, ip_not_allowed, scope. Each endpoint's description names the ones it uses. |
| details | An array of { path, message } on 400 validation_error, one per failing field, with path dotted for nested fields (to.phone). On 402 plan_limit_reached the single entry carries a limit; on a duplicate-contact 409it carries the existing contact's id. |
{
"error": {
"code": "unprocessable",
"message": "The 24-hour customer-service window has closed; send a template instead",
"request_id": "req_01J9X4R0F6VW9C3T",
"reason": "outside_window"
}
}Handling errors
- Branch on
code, then onreason. Never onmessage. 429and500are safe to retry — with the sameIdempotency-Keyon a POST — after theRetry-Afterthe 429 carries.400,404and422will not succeed on retry; fix the request.401and403are configuration problems — the key, its scopes or its allowlist — not transient ones.