Skip to content
Sendozi

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.

By SendoziUpdated 3 min read

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 and failure
Success
{ "success": true, "data": { }, "request_id": "req_8ecdcb4ce2ae4290" }
Failure
{
  "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

Every code the API returns
CodeStatusMeaningRetry?
unauthorized401Missing or invalid API keyNo - fix the key
forbidden403Authenticated but not permittedNo
invalid_request400Malformed body or parametersNo - fix the payload
invalid_recipient400Not a valid Nigerian mobile numberNo - normalise the recipient
message_policy_violation400Content failed policy checksNo - change the content
not_found404No such resourceNo
method_not_allowed405Wrong HTTP methodNo
idempotency_conflict409Key reused with a different body, or first attempt still runningNo - use a new key, or wait
rate_limit_exceeded429Too many requestsYes - after the delay in resolution
insufficient_balance403Wallet does not cover the sendAfter funding
wallet_frozen403The wallet is frozenNo - contact support
kyc_required403KYC not approved, or production access not grantedNo - complete KYC
api_key_blocked403Key blocked by Sendozi OpsNo - contact support
sender_id_not_approved403Sender ID not registered or not approvedNo - use an approved sender
sender_id_route_mismatch403Sender ID is approved for the other SMS routeNo - use a sender approved for this sms_type
sender_id_unclassified403Sender ID predates route classificationNo - Operations records the route, or resubmit
channel_not_active403Channel not enabled for the workspaceNo
recipient_suppressed403Recipient is on your suppression listNo - respect the opt-out
provider_not_configured503Provider unavailable, or platform maintenance modeYes - with backoff
internal_error500Unexpected failureYes - then quote request_id to support
Every code the API returns

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.

  1. Platform maintenance mode
  2. Recipients valid for the channel
  3. Content passes policy checks
  4. No recipient suppressed
  5. API key active
  6. Workspace active
  7. KYC approved and production access granted
  8. Channel active for the workspace
  9. SMS: sender ID approved
  10. Wallet exists and is not frozen
  11. Balance covers the estimated cost
  12. Provider available

Sandbox sends run checks one to four only.

Handling errors in code

Branch on the code, not the message
TypeScript
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.

Make retries safe

Pair error handling with an idempotency key so a retry never sends twice.