Reliability
Errors and the response envelope
Every response uses the same envelope with success, data or error, and a request_id. Each error carries a code, a message and a resolution written for a developer to act on. The code tells you whether to fix the request, wait, or contact support.
The response envelope
Every response, success or failure, has the same shape: a success boolean, either data or error, and a request_id. Write your client against the envelope once and every endpoint behaves consistently.
{ "success": true, "data": { }, "request_id": "req_8ecdcb4ce2ae4290" }{
"success": false,
"error": {
"code": "insufficient_balance",
"message": "The wallet balance does not cover this send.",
"resolution": "Top up the wallet and retry."
},
"request_id": "req_8ecdcb4ce2ae4290"
}- code is the stable, machine-readable identifier. Branch on this, never on the message text.
- message describes what happened.
- resolution says what to do about it. It is written for a developer, not for display to an end user.
- request_id is also returned as the x-request-id header. Log it on every request.
Error codes
| Code | Status | Meaning | Retry? |
|---|---|---|---|
| unauthorized | 401 | Missing or invalid API key | No - fix the key |
| forbidden | 403 | Authenticated but not permitted | No |
| invalid_request | 400 | Malformed body or parameters | No - fix the payload |
| invalid_recipient | 400 | Not a valid Nigerian mobile number | No - normalise the recipient |
| message_policy_violation | 400 | Content failed policy checks | No - change the content |
| not_found | 404 | No such resource | No |
| method_not_allowed | 405 | Wrong HTTP method | No |
| idempotency_conflict | 409 | Key reused with a different body, or first attempt still running | No - use a new key, or wait |
| rate_limit_exceeded | 429 | Too many requests | Yes - after the delay in resolution |
| insufficient_balance | 403 | Wallet does not cover the send | After funding |
| wallet_frozen | 403 | The wallet is frozen | No - contact support |
| kyc_required | 403 | KYC not approved, or production access not granted | No - complete KYC |
| api_key_blocked | 403 | Key blocked by Sendozi Ops | No - contact support |
| sender_id_not_approved | 403 | Sender ID not registered or not approved | No - use an approved sender |
| sender_id_route_mismatch | 403 | Sender ID is approved for the other SMS route | No - use a sender approved for this sms_type |
| sender_id_unclassified | 403 | Sender ID predates route classification | No - Operations records the route, or resubmit |
| channel_not_active | 403 | Channel not enabled for the workspace | No |
| recipient_suppressed | 403 | Recipient is on your suppression list | No - respect the opt-out |
| provider_not_configured | 503 | Provider unavailable, or platform maintenance mode | Yes - with backoff |
| internal_error | 500 | Unexpected failure | Yes - then quote request_id to support |
The order checks run in
A production send is checked in a fixed order and returns the first failure without charging. Knowing the order saves time: an insufficient_balance error means everything before it already passed.
- Platform maintenance mode
- Recipients valid for the channel
- Content passes policy checks
- No recipient suppressed
- API key active
- Workspace active
- KYC approved and production access granted
- Channel active for the workspace
- SMS: sender ID approved
- Wallet exists and is not frozen
- Balance covers the estimated cost
- Provider available
Sandbox sends run checks one to four only.
Handling errors in code
const PERMANENT = new Set([
"invalid_request",
"invalid_recipient",
"message_policy_violation",
"unauthorized",
"forbidden",
"kyc_required",
"sender_id_not_approved",
"sender_id_route_mismatch",
"sender_id_unclassified",
"channel_not_active",
"recipient_suppressed",
"api_key_blocked",
"wallet_frozen",
"idempotency_conflict",
]);
export function classify(error: { code: string }) {
if (PERMANENT.has(error.code)) return "fix";
if (error.code === "insufficient_balance") return "fund";
if (error.code === "rate_limit_exceeded") return "backoff";
return "retry";
}Escalating
When something needs support, the request_id is the single most useful thing you can supply - it is how a failure is traced through the platform logs. Include it, the endpoint, the approximate time and the error code. For a delivery question, include the message id as well.
Frequently asked questions
- What does insufficient_balance mean?
- The wallet balance does not cover the estimated cost of the send, so nothing was sent and nothing was charged. Fund the wallet and retry.
- What is request_id for?
- It identifies one API request across the platform's logs. It is returned in the body and as the x-request-id header, and quoting it is how support traces a specific failure.
- Which Sendozi errors should I retry?
- rate_limit_exceeded after the delay in its resolution, provider_not_configured with backoff, internal_error, and network-level failures. Everything else will fail identically on a retry.
- Why did my send fail with provider_not_configured?
- Either the provider is unavailable or the platform is in maintenance mode. It is transient, so retry with exponential backoff rather than failing the whole batch.
Related reading
Getting started
Quickstart: send your first SMS
Get an API key, send a sandbox SMS, read the response envelope, register a delivery webhook and move to production. A complete first integration in one page.
Reliability
Idempotency
How Idempotency-Key works on Sendozi send endpoints, what happens on a repeat with the same or a different body, how long keys are retained, and how to choose a key.
Reliability
Rate limits and pagination
The Sendozi send rate limit, why it fails closed, how to back off correctly, and how keyset cursor pagination works on list endpoints.
Channels
SMS API reference
Endpoints, request fields, routing, batching, sender ID management and page-based cost for sending SMS to Nigerian numbers through the Sendozi API.