If you have searched for "bulk SMS Kenya," you will have found a hundred providers. Search for a bulk voice call API in Kenya and the results thin out fast. Most platforms treat voice as an afterthought bolted onto SMS, priced in USD, billed to a card you may not have. Sautikit is the opposite: voice-only, billed in KES, topped up over M-Pesa, with a JSON API a backend engineer can wire up in an afternoon.
This guide covers what bulk voice costs in Kenya, how the wallet works, and a runnable quickstart for broadcasting to thousands of numbers.
TL;DR
Outbound calls bill at KES 3/min, billed per second from the moment the call connects; a 20-second broadcast call is about KES 1.
Top up the wallet over M-Pesa; no USD card, no monthly minimum. A zero balance returns 402 before any call is placed.
Bulk = a loop over POST /v1/calls, one request per recipient, with an Idempotency-Key so retries never double-dial.
There is no special "broadcast" endpoint to learn. A bulk campaign is just many single calls: you iterate your recipient list and call POST /v1/calls once per number. Each call dials immediately and returns a call_id before the remote party answers; you track outcomes through the call.answered and call.completed webhooks. Whether you play a pre-recorded greeting, a TTS message, or an interactive menu is decided by the voice actions you return when the call connects.
That design keeps the mental model simple: master one call, and you have mastered a million.
1 GB storage included free; paid tiers from KES 599 / month
A festive greeting blast of 20 seconds per call works out to:
20 seconds × (KES 3 / 60) = KES 1.00 per answered call
10,000 recipients × KES 1.00 = KES 10,000 per campaign
These are illustrative; the pricing page is the single source of truth for current rates and the billing increment. The point that matters for budgeting: you pay for answered talk-time in KES, not for an SMS-style per-message fee, and unanswered calls do not bill the full leg.
Sautikit runs on a prepaid wallet. Before any call leaves the PBX, a pre-flight check confirms your balance: a zero balance returns 402 wallet.insufficient_balance and no call leg is created, so you never get a surprise invoice. Top up over M-Pesa in under a minute; the credit lands in KES and the wallet decrements per call. No card, no USD FX, no monthly commitment.
// quickstart.jsconst BASE = "https://api.sautikit.com";const resp = await fetch(`${BASE}/v1/calls`, { method: "POST", headers: { "Authorization": `Bearer ${process.env.SAUTIKIT_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "demo-call-001", }, body: JSON.stringify({ from: process.env.SAUTIKIT_FROM, // a number your workspace owns to: ["+254700000001"], }),});console.log(resp.status, await resp.json());// 202 { call_id: '9d2b1f53-…', pbx_session_id: 'HD_1a2b3c', status: 'ringing', ... }
A 202 means the PBX accepted the call and is dialing. The status is the live PBX state (ringing), not the final outcome; for that you poll GET /v1/calls/{id} or, better, subscribe to webhooks.
When the call connects, Sautikit POSTs to your voice_callback_url. Return the message to play:
import express from "express";const app = express();app.use(express.urlencoded({ extended: true }));app.post("/voice/greeting", (req, res) => { res.json({ actions: [ { say: { text: "Hello from Acme. Habari kutoka Acme. Thank you for being our customer.", language: "sw-KE", } }, { hangup: {} }, ] });});
Swap the say for a getDigits menu and you have an interactive broadcast: "press 1 to confirm," "press 2 for a callback." See the voice actions reference for the full verb set, and our debt-reminder robocall guide for a press-to-pay example.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));function dial(recipient) { return fetch(`${BASE}/v1/calls`, { method: "POST", headers: { "Authorization": `Bearer ${process.env.SAUTIKIT_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `campaign42:${recipient.id}`, // one dial per recipient }, body: JSON.stringify({ from: FROM_NUMBER, to: [recipient.msisdn] }), });}// Pace the campaign; don't fire 10,000 calls in one burstfor (const recipient of recipients) { dial(recipient); // fire without awaiting; the sleep paces throughput await sleep(50); // ~20 calls/sec; tune to your concurrency allowance}
Two rules for bulk that save you grief:
Idempotency per recipient. Key each call on a stable recipient ID so a re-run or partial failure never double-dials anyone (important for cost and for staying clear of harassment rules).
Pace the dialing. Don't submit the entire list in one burst; throttle to a sustainable calls-per-second so answered calls and webhooks stay manageable.
SMS is unbeatable for one-way, low-urgency text: a receipt, a code, a link. Voice wins when you need attention, interactivity, or reach beyond smartphones: festive greetings that feel personal, payment reminders that can collect, confirmations that cut no-shows, and broadcasts to feature-phone or low-literacy audiences. The two are complements, not competitors.
If you need both channels from one account, that is where Helloduty, the multi-channel CX platform Sautikit is part of, comes in: voice through Sautikit, SMS and WhatsApp through Helloduty, one wallet and one set of contacts.
You don't need one. Sautikit bills in KES and you top up the prepaid wallet over M-Pesa. Calls draw down the balance; a zero balance blocks new calls with a 402 before any spend.
Roughly KES 1 per 20-second answered call (KES 3/min, billed per second from the moment the call connects). A 10,000-recipient greeting blast is about KES 10,000 in talk-time.
Pace to a sustainable calls-per-second rather than bursting the whole list. Throttling keeps webhooks and answered calls manageable and avoids looking like spam to carriers.