Event types
sms.queued
sms.queued
Fires the moment we accept a
POST /v1/sms. Useful for reconciling “we intended to send X” vs “we actually sent X” in your audit log.sms.sent
sms.sent
The upstream carrier accepted the message. This is not delivery — just that the carrier acknowledged the handoff.
sms.delivered
sms.delivered
The handset confirmed receipt (via the carrier’s delivery report). This is the event you care about for OTP flows.
sms.failed
sms.failed
A permanent failure after send — the carrier tried but couldn’t deliver. Typically: handset off too long, SIM no longer exists, blacklist.Wallet overage is automatically refunded for permanent failures.
sms.rejected
sms.rejected
Pre-send rejection — e.g. Also refunds overage automatically.
phone_invalid, dnd_blocked, or carrier-level sender-ID block. No attempt was made to deliver.Payload envelope
Every event has the same top-level shape:data matches the shape of GET /v1/sms/:id — if you’ve typed your fetch code, the webhook shape is the same.
Signature verification
Same scheme as email:Robase-Signature: t=<unix>,v1=<hex-hmac>, where the HMAC is over {t}.{body} with your webhook’s secret. See Webhooks for the verification code per language.
Common patterns
OTP flow
Campaign monitoring
Delayed DLRs
African carriers sometimes take minutes or even hours to confirm delivery. Don’t tie UI behavior tosms.delivered — use sms.sent as the “we handed it off” signal, and treat sms.delivered as asynchronous confirmation that may arrive later.
For OTP-style flows where you need fast feedback, show the user “Code sent — check your messages” as soon as you get sms.sent, and only escalate to “Didn’t get it?” after a 60-second client-side timeout.
Test-mode webhooks
Test-mode sends fire the same events, withtest_mode: true in data. Use this to write idempotent webhook handlers — your real production traffic will go through the same code path with the flag off.