RaabtaHQ
Errors

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.

400 Bad Request
{
  "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"
      }
    ]
  }
}
NameTypeDescription
errorrequired
objectWhat went wrong
error.coderequired
stringStable machine-readable code — branch on this
error.messagerequired
stringHuman-readable; may be reworded
error.request_idoptional
stringQuote this in support conversations
error.reasonoptional
stringFiner-grained machine string for 422s and 409s
error.detailsoptional
object[]Per-field failures
error.details[].pathrequired
stringDotted path to the failing field, or the resource name for a limit
error.details[].messagerequired
stringHuman-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.

StatusCodeWhen
400bad_requestThe request could not be parsed: malformed JSON, an invalid cursor, or a query parameter of the wrong shape.
400validation_errorThe body or query failed validation. `details` lists each failing field with a `path` and a `message`.
401unauthorizedNo usable API key: the Authorization header is missing or malformed, or the key is unknown, revoked or expired. The three are deliberately indistinguishable.
402plan_limit_reachedThe account has reached a plan limit for this resource. `details[0].limit` is the cap; ask the account owner to raise it.
403forbiddenThe 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.
403account_suspendedThe account this key belongs to is suspended. Rotating the key will not help; contact support.
404not_foundNo such resource in this account. A resource that exists in another account also returns this.
409conflictThe request conflicts with current state: a duplicate phone number on create, an invalid state transition, or an idempotent request that is still in flight.
422unprocessableThe 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`).
422channel_not_connectedThe channel this request needs is not connected on the account. Connect it in Settings and retry.
429rate_limitedThe per-key budget, or the per-IP budget for failed authentication, is exhausted. Honour `Retry-After` before retrying.
500internalSomething 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:

FieldMeaning
reasonA 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.
detailsAn 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.
422 Unprocessable
{
  "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 on reason. Never on message.
  • 429 and 500 are safe to retry — with the same Idempotency-Key on a POST — after the Retry-After the 429 carries.
  • 400, 404 and 422 will not succeed on retry; fix the request.
  • 401 and 403 are configuration problems — the key, its scopes or its allowlist — not transient ones.
Unknown codes will appear as the API grows. Treat a code you do not recognise as a generic failure of its HTTP status class rather than crashing on it — see Versioning.