Skip to Content
GuidesHandling errors

Handling errors

Every error is JSON with a detail field, and the HTTP status tells you whether retrying could ever help.

{ "detail": "Insufficient balance. Need KES 45.00, have KES 12.50." }

Status codes

CodeMeaningRetry?
200 / 201It worked—
202Accepted for delivery and charged. Not yet delivered—
400The request was understood but not allowed in this stateNo, fix it
401The API key is missing, wrong, or the account is suspendedNo
402Not enough wallet balance. Nothing was sent or chargedAfter topping up
403Authenticated, but not permitted to do thisNo
404No such message, campaign or contact — or not yoursNo
409It already existsNo
422The request body didn’t validateNo, fix it
429Too many requestsYes, after a pause
5xxSomething broke on our sideYes, with backoff

Never retry a 402 automatically in a loop. You’ll hammer the API without sending anything. Top up, then retry.

The ones you’ll actually hit

401 Invalid or missing credentials

The key is wrong or absent. Check you’re sending it as X-API-Key, not Authorization:

curl https://api.wakalisms.com/wallet -H "X-API-Key: YOUR_KEY"

If the key was rotated, the old one stopped working immediately. A 401 Account suspended is different: the key is fine, the account is blocked. Contact support.

402 Insufficient balance

{ "detail": "Insufficient balance. Need KES 45.00, have KES 12.50." }

Nothing was sent and nothing was charged. The message tells you the shortfall, so you can surface it or alert yourself. Watch your balance rather than waiting for this:

curl https://api.wakalisms.com/wallet -H "X-API-Key: YOUR_KEY"

422 No valid phone numbers to send to

Every number in the request failed to parse. See Phone numbers. If only some fail, you get a 202 and the failures come back in invalid_recipients — check that field, because a partial failure still looks like success:

const { recipients_count, invalid_recipients } = await send(numbers, text); if (invalid_recipients.length) { console.warn("Not sent to:", invalid_recipients); }

422 from validation

The body didn’t match what the endpoint expects. FastAPI returns an array here, not a string:

{ "detail": [ { "loc": ["body", "message"], "msg": "Field required", "type": "missing" } ] }

Handle both shapes when you display errors:

function errorText(body) { const d = body?.detail; if (typeof d === "string") return d; if (Array.isArray(d)) return d.map(e => e.msg).join("; "); return "Something went wrong"; }

429 Too many requests

Slow down and retry with backoff. Batching helps more than retrying: recipients takes up to 10,000 numbers, so one call beats a thousand.

Retrying safely

Retry 429 and 5xx. Never retry 4xx — the result won’t change.

async function withRetry(fn, { attempts = 4 } = {}) { let wait = 1000; for (let i = 0; i < attempts; i++) { const res = await fn(); if (res.ok) return res; // A refusal we caused won't be fixed by asking again. if (res.status >= 400 && res.status < 500 && res.status !== 429) return res; if (i < attempts - 1) { await new Promise(r => setTimeout(r, wait + Math.random() * 500)); wait *= 2; } } }

A timeout is not a failure. If a /sms/send request times out, the message may well have been accepted and charged. Retrying blindly sends it twice. Check /sms/messages?phone=... before resending.

Partial success

/sms/send returns 202 when any recipient was accepted. Always read the response rather than trusting the status:

{ "recipients_count": 48, "cost_kes": 21.6, "status": "sent", "invalid_recipients": ["0202345678", "not-a-number"] }

And status can be failed inside a 202 if the carrier rejected the whole batch — in which case you were refunded automatically:

if (data.status === "failed") { // Already refunded. Safe to try again. console.error(data.error); }

Getting help

Include the job_id or message id and roughly when it happened — support@wakalisms.com. That’s enough for us to find the exact message in our logs.

Last updated on