SMS
Base path: /sms
Send SMS
POST /sms/send
{
"recipients": ["+254712345678", "0798765432"],
"message": "Your order is ready for pickup.",
"sender_id": null
}recipients— up to 10,000 phone numbers per request. Any format (0712...,254712...,+254712...) is accepted and normalised to E.164.message— up to 918 characters (6 SMS pages).sender_id— optional; defaults to your account’s default Sender ID, or the sharedWakaliLabsID if you haven’t registered your own. When sending via the shared ID, your company name is automatically prefixed to the message body.
202 response:
{
"job_id": "b6b2b6f0-...",
"recipients_count": 2,
"cost_kes": 1.0,
"status": "sent"
}Billing is calculated on the final outgoing message — after the company-name prefix is applied — since a long prefix can push a message over a multi-part boundary. Your wallet is debited upfront and automatically refunded if the send fails.
402 Insufficient balance if your wallet can’t cover the cost. Top up via
POST /wallet/topup.
List messages
GET /sms/messages?page=1&page_size=20
Paginated history of your own messages, newest first. Each item includes status
(queued | sent | delivered | failed), provider_status (raw carrier status, for display),
and source (api | browser).
Get a single message
GET /sms/messages/{message_id} — look up by the message’s own id (not the provider ID).
Returns whatever is currently stored — does not make a live carrier call.
Live delivery status
GET /sms/delivery/{message_id} — looked up by provider message id, not the internal id.
Triggers a live status check against the carrier and syncs the result to our records before
returning. Use this when you need the freshest possible status; use the plain messages endpoints
above for routine polling to avoid unnecessary upstream calls.
Stats
GET /sms/stats — counts of your messages grouped by status:
{ "total": 240, "delivered": 210, "sent": 12, "failed": 15, "queued": 3 }