Healthcare in Kenya operates across a spectrum from urban private hospitals to rural dispensaries where patients share a single community phone. Voice is the single channel that reaches all of them. This post covers three healthcare voice patterns with measurable uptake in Kenya: appointment reminders that reduce DNA ("did not attend") rates by up to 35%, IVR-routed nurse triage lines, and Community Health Worker referral callbacks, with Sautikit implementation and a cost model for a 500-bed hospital.
The argument for voice over SMS in healthcare is not about delivery rates; Safaricom's SMS delivery is generally reliable. It is about comprehension and action rates.
A text reminder requires the patient to read the message, interpret it correctly, and act. For patients with low literacy (a significant share of patients in rural Kenya and among older demographics in peri-urban areas), a text message may be read by someone else, misread, or ignored. A voice call that says "Habari yako. Una miadi katika Kliniki ya Afya Umoja siku ya Ijumaa saa tisa asubuhi. Bonyeza moja kukubali" (Hello. You have an appointment at Umoja Health Clinic this Friday at 9 AM. Press 1 to confirm) is universally understood regardless of literacy level.
Kenya's Ministry of Health data shows appointment DNA rates of 25–40% at public health facilities and 15–25% at private clinics. Studies using voice reminders in Kenya and comparable Sub-Saharan African health systems report DNA rate reductions of 25–35% when reminders include a confirmation prompt that the patient must actively respond to.
The confirmation DTMF also provides actionable data: a patient who presses 2 to reschedule is much less costly to handle than a patient who silently does not appear.
// POST /webhooks/reminder-voicefunc ReminderVoiceHandler(w http.ResponseWriter, r *http.Request) { var body struct { SessionID string `json:"sessionId"` Metadata map[string]string `json:"metadata"` } json.NewDecoder(r.Body).Decode(&body) apptID := body.Metadata["appointment_id"] appt, err := db.GetAppointment(r.Context(), apptID) if err != nil { http.Error(w, "appointment not found", http.StatusInternalServerError) return } dayName := appt.Date.Format("Monday") timeStr := appt.Date.Format("3:04 PM") clinic := appt.ClinicName actions := map[string]interface{}{ "actions": []map[string]interface{}{ { "say": map[string]interface{}{ "text": fmt.Sprintf( "Hello. This is a reminder from %s. "+ "You have an appointment this %s at %s. "+ "Press 1 to confirm you will attend. "+ "Press 2 to reschedule. "+ "Press 3 to cancel.", clinic, dayName, timeStr, ), "language": "en-KE", }, }, { "getDigits": map[string]interface{}{ "numDigits": 1, "timeout": 12000, "finishOnKey": "", "action": os.Getenv("BASE_URL") + "/webhooks/reminder-response", }, }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(actions)}
The confirmation webhook updates the appointment record and, if the patient presses 2 (reschedule), sends an SMS with a rescheduling link and removes the slot from the hold queue:
// POST /webhooks/reminder-responsefunc ReminderResponseHandler(w http.ResponseWriter, r *http.Request) { var body struct { SessionID string `json:"sessionId"` Digits string `json:"digits"` Metadata map[string]string `json:"metadata"` } json.NewDecoder(r.Body).Decode(&body) apptID := body.Metadata["appointment_id"] var responseText string switch body.Digits { case "1": db.ConfirmAppointment(r.Context(), apptID) responseText = "Thank you. Your appointment is confirmed. See you then." case "2": db.ReleaseAppointmentSlot(r.Context(), apptID) sendReschedulingSMS(apptID) // sends SMS with booking link responseText = "We will send you a message with a link to reschedule. Goodbye." case "3": db.CancelAppointment(r.Context(), apptID) responseText = "Your appointment has been cancelled. To rebook, please call us. Goodbye." default: responseText = "We did not receive your response. Please call us to confirm. Goodbye." } actions := map[string]interface{}{ "actions": []map[string]interface{}{ {"say": map[string]interface{}{"text": responseText, "language": "en-KE"}}, {"hangup": map[string]interface{}{}}, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(actions)}
The Kenya Data Protection Act 2019 classifies health data as sensitive personal data under Section 46. This has two direct implications for voice reminder calls:
Recording requires explicit consent. If you add a Record verb to reminder calls (for quality assurance), the call script must include a disclosure ("This call may be recorded") before recording begins.
Sautikit recording URLs are time-limited signed URLs. Recordings are not permanently public. Each call to GET /v1/calls/{id}/recording returns a fresh URL valid for 15 minutes. This means you cannot store the recording URL in your database as a permanent link; store the call_id and re-fetch the signed URL when needed. This design ensures that even if your database is compromised, the stored call_id cannot be used to retrieve patient audio without a valid Sautikit API key.
For appointment reminder calls that do not include recording, the only personal data processed by Sautikit is the patient's phone number (E.164 format) and the call metadata you pass in the metadata field. Sautikit's infrastructure operates within Kenya: recordings are stored in Nairobi-region object storage, satisfying the data residency requirements in Regulation 16 of the Data Protection (General) Regulations 2021.
A nurse triage line allows patients to call a central number, describe their symptoms via a structured IVR, and be routed to the appropriate care level: self-care advice, appointment booking, or urgent referral.
The IVRFlowDiagram /> below shows the routing logic:
IVR menu flow
Each digit press posts back to your voice_callback_url. Your server reads the Digits field and returns the next action set.
The inbound call flow uses a series of Say + GetDigits pairs to collect symptom severity signals:
{ "actions": [ { "say": { "text": "Welcome to the health triage line. Please listen carefully and press the number that matches your situation. Press 1 if you are experiencing chest pain or difficulty breathing. Press 2 if you have a fever above 38 degrees. Press 3 for other symptoms. Press 4 to speak to a nurse now.", "language": "en-KE" } }, { "getDigits": { "numDigits": 1, "timeout": 15000, "finishOnKey": "", "action": "https://your-server.example.com/webhooks/triage-level-1" } } ]}
If the patient presses 1 (chest pain or breathing difficulty), the IVR immediately transfers to a nurse via Dial; no further branching. If they press 2 or 3, the IVR continues with secondary questions to determine urgency. If they press 4, immediate transfer.
Inbound calls are free (KES 0/min), so a nurse triage line handling 200 inbound calls/day at any call length costs nothing in call minutes:
IVR self-service calls (tier-0, no nurse): 140 calls/day × any duration × KES 0 = KES 0/day
Escalated calls (30% reach a nurse): 60 calls/day × KES 0/min = KES 0/day
Daily call cost: KES 0/day; recording storage and number rental remain
Compare this to the cost of a two-nurse triage team:
Two nurses at the minimum healthcare wage of approximately KES 30 000/nurse/month = KES 60 000/month
Office costs, supervision, and benefits: approximately KES 20 000/month
Total human cost: approximately KES 80 000/month
The hybrid model (IVR handling 70% of calls at tier-0, nurses handling 30% of escalations) costs approximately KES 67 500/month for the voice channel and reduces the nursing team need to one nurse rather than two, saving roughly KES 12 500/month while improving response times for complex cases.
Community Health Workers (CHWs) in Kenya's healthcare system are the last mile of primary care. They visit households, identify patients who need clinical attention, and refer them to the nearest facility. The gap is follow-through: a CHW who verbally refers a patient has no mechanism to confirm the referral was completed.
A Sautikit callback flow closes this loop. When a CHW logs a referral in a CHW management system, the platform:
Places an outbound call to the referred patient
Confirms the appointment
Records the confirmation for the CHW's case file
// Place a referral confirmation call to a patient.// chwId is passed in metadata for case-file correlation.async function placeReferralCallback(patientPhone, facilityName, chwId) { const resp = await fetch("https://api.sautikit.com/v1/calls", { method: "POST", headers: { Authorization: "Bearer " + process.env.SAUTIKIT_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ from: process.env.SAUTIKIT_HEALTH_NUMBER, to: [patientPhone], voice_callback_url: `${process.env.BASE_URL}/webhooks/referral-voice`, metadata: { facility: facilityName, chw_id: chwId, }, }), }); if (!resp.ok) { throw new Error(`sautikit returned ${resp.status}`); } return resp.json();}
The referral voice flow:
import express from "express";const app = express();app.use(express.urlencoded({ extended: true }));app.use(express.json());app.post("/webhooks/referral-voice", (req, res) => { const facility = req.body?.metadata?.facility || "the health facility"; res.json({ actions: [ { say: { text: `Hello. Your Community Health Worker has referred you to ${facility} ` + "for a health check. Press 1 if you plan to attend this week. " + "Press 2 if you need help arranging your visit.", language: "en-KE", }, }, { getDigits: { numDigits: 1, timeout: 12000, finishOnKey: "", action: `${process.env.BASE_URL}/webhooks/referral-confirm`, }, }, ], });});
A 500-bed hospital with 40 outpatient appointments/bed/month runs approximately 20 000 appointments/month.
Voice activity
Volume/month
Avg duration
Cost
Appointment reminders
20 000 calls
35 sec
KES 46 667
Reschedule follow-ups (15% of reminders)
3 000 calls
25 sec
KES 5 000
CHW referral callbacks
500 calls
30 sec
KES 1 000
Recording storage (reminders only, compressed)
~11 700 min
n/a
KES 0 (within 5 GB free)
Total
KES 52 667/month
For context, a single hospital bed generates roughly KES 15 000–30 000 in outpatient revenue per month. Reducing DNA rates by 30% on 20 000 appointments (at an average appointment value of KES 1 500) recovers KES 9 million in appointments that would otherwise be wasted slots. The voice reminder budget of KES 52 667 is under 0.6% of that recovered revenue.
Reschedule follow-ups in these flows lean on SMS, and CHW case files often span WhatsApp and a human-agent desk. For those channels alongside voice, Helloduty is the in-family platform that adds SMS, WhatsApp, USSD, and ticketing.
The minimum viable implementation is a morning cron job that queries the next day's appointments and places a reminder call for each one. The quickstart guide covers initial setup. The voice-actions reference documents GetDigits, Say, and Redirect. Recording storage tiers are at /pricing.
For DPA 2019 compliance, add two items to your privacy policy: a disclosure that reminder calls may be placed to the number on file, and, if recording is enabled, the retention period for voice recordings.