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.
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.
Is there a bulk or broadcast endpoint?
No special endpoint; you loop POST /v1/calls, one request per recipient. This keeps the API small and the same code works for one call or a million.
How do I pay if I don't have a USD card?
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.
What does a bulk voice campaign cost in Kenya?
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.
How fast can I dial?
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.
Can the call be interactive, not just a recording?
Yes. Return a getDigits action and you get DTMF menus ("press 1 to confirm"), including press-to-pay via M-Pesa STK push.