Kenya Power's roughly 10 million customers averaged 3.57 outages per customer per month in FY2024/25 — 2.2 times EPRA's own benchmark — while planned-interruption notices go out as PDF lists posted on X. This guide covers the mass-notification patterns that close that gap with voice: area-scoped broadcast lists, pacing so you do not flood one exchange, dedup across overlapping areas, restoration-confirmation calls, opt-out mechanics, delivery evidence for regulators, and the cost math for a 5,000-customer feeder.
Per EPRA's Bi-Annual Energy and Petroleum Statistics Report for FY2024/25 (published September 2025), Kenya Power had 10,045,775 grid-connected customers by June 2025, and the average customer experienced 3.57 outages per month — down from 3.96 the year before, yet still 2.2x EPRA's own benchmark of 1.63. Multiply the two figures and you get roughly 36 million customer-outage events every month.
The planned share of those interruptions is announced through PDF schedules on the utility website, daily posts on X, and newspaper notices — a broadcast model that assumes the customer goes looking. It fails visibly enough that third-party services like Nijulishe KE and Power Updates Kenya exist purely to scrape those schedules and push alerts. When volunteers build your notification layer, the demand is not in question. And a voice call is the channel that reaches the mama mboga whose fridge stock will spoil — offline, non-literate, or simply not on X.
The stakes are rising. The draft Energy (Electricity Reliability, Quality of Supply and Service) Regulations, 2025 propose compensation of up to 75% of average daily consumption for prolonged unplanned outages, with breaches reported within 30 days. These regulations were still in draft as of mid-2026 — verify their current status at epra.go.ke before treating any clause as law — but the direction is clear: if utilities may one day owe money for outages customers were not warned about, provable notification stops being customer care and becomes compliance infrastructure.
A planned-outage notice is a scheduled, area-scoped voice broadcast. Three building blocks:
Area-scoped contact lists. Your source of truth maps customers to network topology — feeder, transformer, or DMA (district metered area) for water. Each planned interruption resolves to one or more areas, and each area resolves to a phone list. The calling layer only ever sees "this outage event, these MSISDNs."
Schedule windows. Notices should land 24–48 hours before the interruption, inside civil hours — nobody wants a maintenance notice at 22:00. Place calls from a worker you schedule yourself (pattern below), or, for a conversational campaign where customers can ask questions, use the broadcast API: POST /v1/broadcasts — fronted by a published AI agent — takes schedule_days, daily_start_minute/daily_end_minute, timezone: "Africa/Nairobi", and max_attempts/backoff_minutes/retry_on for automatic no_answer and busy retries.
The call itself. For a fixed notice, a plain voice-actions response — say the schedule bilingually, offer a replay, offer an opt-out — is all you need. Point your caller number's voice_callback_url at the handler via PUT /v1/numbers/{id}/routing; routing lives on the number, not on each call.
An outage list is geographically concentrated by definition. Fire 5,000 simultaneous calls at one estate and you congest the very cell sites your customers are on, pile busy results into your stats, and light up your own inbound lines with confused callbacks. Pace the broadcast:
npm install @sautikit/node@0.2.0
// notify-feeder.js — pace a planned-outage broadcast, 5 workers, ~2 calls/secimport { SautikitClient, SautikitError } from "@sautikit/node";const client = new SautikitClient({ apiKey: process.env.SAUTIKIT_API_KEY });const CONCURRENCY = 5;const GAP_MS = 2500; // per-worker gap between placementsasync function callCustomer(msisdn, outageId, pass = 1) { try { return await client.calls.create({ from: "+254709000100", to: [msisdn], // Re-running THIS pass cannot double-call anyone for 24 h; a // deliberate retry pass carries a new pass number, so it dials. idempotencyKey: `outage:${outageId}:${msisdn}:p${pass}`, }); // { call_id, session_id, status: "ringing" } } catch (err) { if (err instanceof SautikitError && err.status === 402) { throw new Error("Wallet empty. Top up, then re-run."); } throw err; }}async function notifyArea(contacts, outageId, pass = 1) { // Dedupe overlapping area lists before dialing anything. const queue = [...new Set(contacts.map((c) => c.msisdn))]; const workers = Array.from({ length: CONCURRENCY }, async () => { while (queue.length) { const msisdn = queue.shift(); await callCustomer(msisdn, outageId, pass); await new Promise((r) => setTimeout(r, GAP_MS)); } }); await Promise.all(workers);}
Five workers, one placement each per 2.5 seconds, is about 120 calls a minute — a 5,000-line feeder clears in roughly 40 minutes without hammering one exchange. Tune concurrency to area size; network realities across Kenya covers why concentrated dialing behaves differently across carriers.
Outage areas overlap: a substation job takes out three feeders, an estate sits on two lists, a customer has two service points on one phone number. Two layers of protection:
List-level: dedupe MSISDNs per outage event before dialing (the Set above).
Platform-level: the Idempotency-Key header. Keyed on outage:{event_id}:{msisdn}:p{pass}, a duplicate placement — a crashed script re-run, a retried cron, the same customer on two lists sharing an event id — replays the original 202 instead of placing a second call, flagged with X-Idempotency-Replayed: true. Dedup holds for 24 hours.
The key protects one pass from re-runs; a deliberate retry pass is a new attempt, so include the pass number in the key — otherwise the 24-hour replay window will swallow it and the retry never dials. One physical interruption = one event_id, however many areas it touches; derive keys from the event, not the list.
The handler below serves the whole call. First hit plays the notice and collects one digit; the platform then POSTs back to the same URL with Digits set, and the handler dispatches on it.
import express from "express";import { sauti } from "@sautikit/node";const app = express();app.use(express.json()); // voice callbacks arrive as JSONapp.post("/voice/outage", (req, res) => { const digits = req.body.Digits || ""; if (!digits) { return res.json( sauti .getDigits({ timeout: 6, numDigits: 1 }, [ // seconds { say: { text: "Habari. This is a planned maintenance notice from Acme Power. " + "Electricity in the Kariobangi area will be interrupted on Tuesday, " + "sixth October, from nine in the morning to five in the evening. " + "Stima itakatika Jumanne tarehe sita Oktoba, saa tatu asubuhi " + "hadi saa kumi na moja jioni. " + "To hear this again, press 1. To stop receiving these calls, press 9.", language: "en-KE", }, }, ]) .say("Asante. Goodbye.", { language: "en-KE" }) .hangup(), ); } if (digits === "1") { // Replay: the fresh hit has no Digits, so the notice plays again. return res.json(sauti.redirect("https://alerts.example.co.ke/voice/outage")); } if (digits === "9") { // Persist the opt-out for this MSISDN before responding. return res.json( sauti .say("You will no longer receive these calls. Umeondolewa.", { language: "en-KE" }) .hangup(), ); } return res.json(sauti.say("Asante. Goodbye.", { language: "en-KE" }).hangup());});
Script rules that survive contact with reality: lead with who you are — an unknown number reading a schedule sounds like a scam until it identifies itself — state the area and window in both English and Swahili, and keep it under 40 seconds. You are billed per second after connect, so brevity is also thrift.
The pattern utilities skip, and the one customers remember: when the crew closes the job, run a second, shorter broadcast to the same deduped list. "Power in Kariobangi has been restored. If you still have no electricity, press 1." A 1 is a still-out report tied to a verified customer and a known transformer — better-structured than the SMS-to-95551 inbound channel, because it arrives area-tagged, minutes after energization. Feed the press-1 list to the crew before they leave the area; a return visit an hour later beats a fresh truck roll tomorrow.
It is also your evidence bookend: notice call before, restoration call after, and the outage duration in between established by your own records rather than reconstructed from complaints.
Outage notices to your own account holders are service communication, not marketing — but Kenya's Data Protection Act, 2019 still applies to the personal data involved, and goodwill applies to everything. Practical mechanics:
Basis and records. Notification uses the phone number the customer gave you at connection. Record that provenance; see shipping compliant voice in Kenya for the wider CAK and DPA picture.
Opt-out anyway. Press-9 costs one menu line and buys trust. Honour it across all outage campaigns for that MSISDN, immediately, and log the timestamped call that carried it — the withdrawal record is itself evidence.
Opt back in. Publish an inbound line (inbound calls are free on Sautikit) where a customer can re-subscribe or update their number with one keypress.
Tenants versus account holders. The person who suffers the outage is often not the registered account holder — landlord accounts, shared meters. Let customers register additional alert numbers; estates and facility managers will do it in bulk for you.
If the draft EPRA compensation regime — or a customer dispute, or an internal SLA — ever asks "did you notify this customer?", you want machine records, not a screenshot of a tweet. Two sources:
Per-number events. Set events_url on your caller number's routing to receive call.started, call.answered, call.completed, and call.failed webhooks. The normalized envelope carries event_id, occurred_at, and a data block with status, duration_seconds, answered_at, and ended_at. Verify the X-Sautikit-Signature header, dedupe on event_id, and archive events keyed by outage — webhook retry semantics covers making that pipeline lossless.
The call log.GET /v1/calls filters by status and time window, so reconciling "who did we reach for outage X" is one query per campaign.
Outcome
What it proves
Follow-up
completed, 25+ seconds
Notice delivered and heard through
None
completed, under 10 seconds
Answered, likely hung up early
Optional SMS follow-up
no_answer
Attempted, not reached
Retry next window
busy
Attempted during congestion
Retry with wider pacing gap
failed
Number problem
Flag for data cleanup
A signed, timestamped call.completed event with a 32-second duration, tied to a customer account and an outage id, is the difference between "we posted the schedule" and "we notified this customer at 10:42 on the 4th, and here is the record." The whole loop — pull the affected list, place the paced calls, read back outcomes — can also be driven from Claude through the Sautikit MCP server, handy for a one-off feeder broadcast with nothing deployed.
Sautikit pricing (as of 2026-06-30): outbound calls KES 3.00/min — KES 0.05 per second, billed per second after connect; inbound free; local Nairobi number KES 100/month ex. VAT (KES 116 incl. VAT). See /pricing for current rates. Assume a 30-second notice:
Line item
Arithmetic
Cost
First pass: 60% of 5,000 answer
3,000 × 30 s × KES 0.05/s
KES 4,500
Retry pass: 40% of the 2,000 misses answer
800 × 30 s × KES 0.05/s
KES 1,200
Unanswered attempts
no connect, no talk-time charge
KES 0
Caller number (monthly, incl. VAT)
flat
KES 116
Total for the campaign
3,800 of 5,000 reached (76%)
≈ KES 5,816
That is about KES 1.53 per confirmed notification — KES 1.16 per customer on the list — with a delivery record for every attempt. A 15-second restoration pass adds roughly KES 2,850 (3,800 × 15 s × KES 0.05/s). Against the exposure the draft regulations contemplate, or one avoidable truck roll, the notification budget rounds to noise. The wallet is prepaid in KES over M-Pesa, so spend is capped by design: about KES 9,000 of float covers the whole feeder, no card on file.
Everything above transfers directly. Nairobi Water publishes interruption notices the same way — PDFs, press releases, posts on X (the October 2025 Kabete-line shutdowns, for example) — and announced its 15 October 2025 mass-disconnection drive on bill defaulters through the media rather than to the customers being disconnected. A pre-disconnection courtesy call collects payment for less than a disconnect-and-reconnect crew visit, and a DMA-scoped interruption broadcast is the feeder pattern with valves instead of transformers.
Estate and facility managers run the same playbook at 200–2,000 households: water-trucking schedules, generator maintenance windows, borehole pump repairs. What matters at that scale is entry cost — a number claimed instantly through the API at KES 116/month incl. VAT and a prepaid M-Pesa wallet, no procurement cycle. And when the voice notice needs a written companion residents can scroll back to, that is Helloduty, the multi-channel CX platform Sautikit is part of.