Reliability
Errors and the response envelope
Every response has success, then data or error, and a request_id. An error carries a stable code, a message and a resolution. 4xx codes are answers about your request and will repeat if resent unchanged; 429 and 5xx are worth retrying with the same Idempotency-Key.
The response envelope
{ "success": true, "data": { }, "request_id": "req_8ecdcb4ce2ae4290" }{
"success": false,
"error": {
"code": "insufficient_balance",
"message": "Wallet balance is not enough for this send.",
"resolution": "Fund the wallet and retry."
},
"request_id": "req_8ecdcb4ce2ae4290"
}HTTP/1.1 429 Too Many Requests
Retry-After: 23
{
"success": false,
"error": {
"code": "rate_limit_exceeded",
"message": "Too many API send requests for this workspace.",
"resolution": "Try again in about 23 seconds.",
"retry_after_seconds": 23
},
"request_id": "req_2f1e0d9c8b7a6958"
}codeis stable. Branch on it, never onmessageorresolution, which may be reworded.resolutionis written for your developers, not for your end users.retry_after_secondsand theRetry-Afterheader come with rate-limit refusals.request_idis also sent as thex-request-idheader. Log it with every call. To use your own, sendX-Request-IDmatching[A-Za-z0-9_-]{8,64}; anything else is replaced.
Error codes
| Code | Status | Meaning and fix |
|---|---|---|
| unauthorized | 401 | No key, or an unrecognised key. Send Authorization: Bearer sk_... |
| api_key_blocked | 403 | The key is revoked, expired or blocked. Use an active key |
| insufficient_scope | 403 | The key lacks the scope this endpoint needs. Create a key with it |
| ip_not_allowed | 403 | A live key used from an address that is not on your IP allowlist. The message names the address; call from an allowed server or add it in the Console |
| workspace_suspended | 403 | The workspace cannot send. Contact Sendozi |
| invalid_request | 400 | A missing or malformed field, invalid JSON, a message over the length limit, or an invalid cursor. The message says which |
| payload_too_large | 413 | The body is over 32 MB. Split the recipients across requests |
| too_many_recipients | 400 | More than 1,000,000 recipients in one request |
| not_found | 404 | No such endpoint, or no such resource in your workspace |
| method_not_allowed | 405 | The endpoint does not accept this HTTP method |
| Code | Status | Meaning and fix |
|---|---|---|
| invalid_recipient | 400 | No valid Nigerian mobile number in the request |
| recipient_suppressed | 400 | Every recipient has opted out of your SMS |
| recipient_dnd | 400 | Live promotional send where every recipient returned a Do Not Disturb block to your workspace in the last 90 days |
| message_policy_violation | 400 | The content matched a blocked phrase. Change the wording |
| kyc_required | 403 | Live sending needs approved KYC and production access |
| sender_id_not_approved | 403 | The Sender ID is not approved for live SMS |
| sender_id_route_mismatch | 403 | A transactional send on a Sender ID approved for promotional only |
| sender_id_airtel_not_whitelisted | 403 | A promotional send on a Transactional Sender ID whose every recipient is on Airtel, which has not yet whitelisted it for promotional |
| sender_id_unclassified | 403 | The Sender ID has no route yet; Sendozi classifies it, or submit a new request |
| channel_not_active | 403 | SMS is not active for the workspace, or sandbox is disabled platform-wide |
| insufficient_balance | 403 | The wallet does not cover the estimated cost. Fund it and retry |
| wallet_frozen | 403 | The wallet is frozen. Contact Sendozi |
| Code | Status | Meaning and fix |
|---|---|---|
| transactional_idempotency_required | 400 | Send an Idempotency-Key |
| transactional_bulk_not_allowed | 400 | Exactly one recipient per transactional request |
| rate_limit_exceeded | 429 | The workspace's transactional limit per minute or per day is reached; wait for Retry-After |
| Code | Status | Meaning and fix |
|---|---|---|
| idempotency_conflict | 409 | The key was used with a different body, or the first request is still running. See idempotency |
| batch_not_cancellable | 409 | The batch has already finished or been cancelled |
| rate_limit_exceeded | 429 | Over a rate limit. Wait for Retry-After |
| queue_unavailable | 503 | The send could not be queued; nothing was sent or charged. Retry with the same key |
| wallet_unavailable | 503 | The wallet could not be reserved. Retry shortly |
| provider_not_configured | 503 | Sending is paused for maintenance, or a platform dependency is unavailable. Retry with backoff |
| DELIVERY_TEMPORARILY_UNAVAILABLE | 503 | SMS delivery is temporarily unavailable. Retry shortly |
| internal_error | 500 | An unexpected failure. Retry once, then contact support with the request_id |
Codes about a message's delivery, such as INVALID_RECIPIENT or delivery_expired, are not request errors. They appear on the message itself; see why a message failed.
What to retry
- Retry
- Network errors, timeouts, 429 (after Retry-After), and 500, 502, 503 and 504, with the same Idempotency-Key and exponential backoff.
- Fix, then send
- Every other 4xx. Resending unchanged returns the same error.
- Wait for the first
- 409 idempotency_conflict with the message "still being processed": the original request is running. Retry in a few seconds.
export function classify(status, error) {
if (status === 429 || status >= 500) return "retry";
if (error.code === "insufficient_balance") return "fund-wallet";
if (error.code.startsWith("sender_id_")) return "fix-sender-id";
return "fix-request";
}Asking for help
Include the request_id, the endpoint, the time and the error code. For a delivery question, include the message_id or batch_id. The request_id is how a request is found in Sendozi's logs.
Frequently asked questions
- What does insufficient_balance mean?
- The wallet balance does not cover the estimated cost of the whole send, so nothing was queued or charged. Fund the wallet and send again.
- Which Sendozi errors should I retry?
- Network errors, timeouts, 429 after the Retry-After delay, and 5xx responses, each with the same Idempotency-Key. Other 4xx errors will repeat until the request is fixed.
- Why did my send fail with provider_not_configured?
- Sending is paused for maintenance or a platform dependency is briefly unavailable. It is temporary: retry with backoff and the same Idempotency-Key.
- What is request_id for?
- It identifies one API request in Sendozi's logs. It is in every response body and in the x-request-id header. Quote it when you contact support.
Related reading
Reliability
Idempotency and safe retries
How the Idempotency-Key header works on Sendozi send endpoints, what a repeat returns, why the body must be byte-for-byte identical, how long keys last, and how to choose one.
Reliability
Limits, large sends and pagination
The Sendozi request limits, what 429 and Retry-After mean, how a send of up to 1,000,000 recipients is accepted and paced out to the networks, and how cursor pagination works.
Sending
Sending SMS
POST /v1/sms/send and /v1/sms/bulk: every request field, the response, how recipients are cleaned, limits, the delivery window, and exactly how a send is priced and charged.
Sending
Transactional SMS: OTPs, alerts and receipts
What a transactional send needs: a Transactional Sender ID, one recipient, your own message and an Idempotency-Key. No templates to register. With a working OTP example.