Errors
Every 4xx and 5xx response has the same shape:
{ "error": { "type": "validation_error", "code": "missing_content", "message": "content is required" }}| Field | Use it for |
|---|---|
type | Coarse category. Branch on this for broad handling — auth vs. validation vs. billing. |
code | The contract. Stable, machine-readable. Branch on this for specific handling. |
message | Human-readable, may include your input. Log it; show it to developers. Never parse it, and never show it to end users. |
Handling errors well
Section titled “Handling errors well”const res = await fetch(url, opts);const body = await res.json();
if (!res.ok) { const { type, code, message } = body.error;
switch (code) { case 'insufficient_balance': case 'credit_limit_exceeded': await alertFinanceTeam(message); // human problem — stop sending break;
case 'sender_id_invalid': await alertOps(message); // config problem — stop sending break;
case 'invalid_destination': await quarantineNumber(recipient); // data problem — skip this one, continue break;
default: if (res.status >= 500 || res.status === 429) { await retryWithBackoff(); // transient — safe to retry } else { throw new Error(`${type}/${code}: ${message}`); } }}The important distinction is retryable vs. not. Retrying a
missing_content will fail forever and just burn your rate limit. Retrying a
503 will very likely succeed.
Categories
Section titled “Categories”type | HTTP | Meaning | Retry? |
|---|---|---|---|
validation_error | 400 | Your request was malformed or missing a field. | ❌ Fix the request |
authentication_error | 401 | Token missing, malformed, or revoked. | ❌ Fix the token |
payment_required | 402 | Account can’t fund the request. | ❌ Top up first |
not_found | 404 | The resource doesn’t exist. | ❌ |
pipeline_rejected | 400 | Accepted syntactically, rejected by routing/policy. | ❌ Usually |
service_unavailable | 503 | A dependency is temporarily down. | ✅ With backoff |
internal_error | 500 | Our fault. | ✅ With backoff |
Validation errors
Section titled “Validation errors”Your request is wrong. Fix it and resend — retrying unchanged will always fail.
| Code | What happened | How to fix it |
|---|---|---|
invalid_request_body | The JSON didn’t parse, or a field had the wrong type. | Check Content-Type: application/json and that to is an array. |
missing_content | content was absent or empty. | Provide non-empty content. |
missing_recipients | to was absent or an empty array. | Provide at least one destination. |
missing_destination | A recipient entry was empty. | Remove empty strings from to. |
missing_sender_id | from was absent. | Provide an approved sender ID. |
sender_id_invalid | from is not approved for this account. | Register it under Messaging → Sender IDs and wait for approval. See Sender IDs. |
invalid_destination | An MSISDN was malformed. | Use international format — 2348020000000. See Phone numbers. |
no_valid_recipients | Every destination failed validation. | Nothing was sent. Fix the numbers. |
no_known_operators | No destination prefix matched a Nigerian network. | Check the numbers are real Nigerian MSISDNs. |
too_many_recipients | The batch exceeded the per-request limit. | Split into smaller batches. |
missing_reference_id | Status was requested without a reference_id. | Include it in the path. |
Authentication errors
Section titled “Authentication errors”| Code | What happened | How to fix it |
|---|---|---|
unauthorized | Token missing, malformed, expired, or revoked. | Send Authorization: Bearer <token>. Re-copy it from Settings → Security. |
account_disabled | The account has been suspended. | Contact your account manager — no code change will fix this. |
Billing errors
Section titled “Billing errors”Nothing was sent and nothing was charged. These need a human, not a retry.
| Code | What happened | How to fix it |
|---|---|---|
insufficient_balance | Balance doesn’t cover the batch. | Top up. Check cost_estimate.sufficient before sending to catch this early. |
credit_limit_exceeded | Postpaid credit limit reached. | Contact your account manager. |
credit_limit_not_configured | Postpaid account with no limit set. | Account setup issue — contact support. |
tariff_not_configured | No price configured for that operator. | Contact support; we can’t price the message. |
Routing & pipeline errors
Section titled “Routing & pipeline errors”The request was valid but couldn’t be dispatched.
| Code | What happened | How to fix it |
|---|---|---|
pipeline_rejected | Rejected by a routing or policy rule. | Read message for the specific rule. Often a blocked sender/content policy. |
batch_not_found | No batch with that reference_id. | Check the ID. Note only batches sent via the REST API are visible here. |
Transient errors
Section titled “Transient errors”These are the retryable ones. Back off exponentially — and reuse your
Idempotency-Key.
| Code | What happened | How to fix it |
|---|---|---|
status_lookup_unavailable | Status store is temporarily unreachable. | Retry in a few seconds. Your batch is unaffected. |
redis_required | A required dependency is down. | Retry with backoff. |
voice_unavailable | The voice pipeline is temporarily unavailable. | Retry with backoff. |
internal_error | Unhandled server error. | Retry with backoff. If it persists, send us the reference_id. |
reference_id_generation_failed | Couldn’t mint a reference ID. | Retry. Nothing was sent. |
Rate limits
Section titled “Rate limits”Exceed your account’s throughput and you’ll get 429. Back off and retry —
honour Retry-After (in seconds) when present.
Batches are queued and dispatched FIFO at your account’s rate limit, so a large
batch doesn’t need client-side throttling. It’s concurrent API calls that get
limited, not messages within one batch. Prefer one request with 1,000 recipients
over 1,000 requests with one recipient each — it’s faster, cheaper on your rate
limit, and gives you a single reference_id to track.