---
title: >-
  Cut missed appointments by 35% in Kenyan healthcare: voice reminders, triage
  lines, and referral calls
description: >-
  Three healthcare voice patterns for Kenya: appointment reminders that cut DNA
  rates, IVR triage lines, and CHW referral callbacks, with code.
summary: >-
  Voice reaches every patient in Kenya regardless of smartphone ownership. This
  post covers three healthcare voice patterns with the highest uptake:
  reminders, triage, and CHW referral.
date: 2026-07-03T00:00:00.000Z
type: blog
---


## Summary

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.

## Why Voice Beats SMS for Kenyan Healthcare Reminders

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.

## Pattern 1: Appointment Reminder Calls with Confirmation DTMF

The reminder call flow has three states: confirm (1), reschedule (2), cancel (3). Each drives a different downstream action in the appointment system.

Place the call with the appointment ID in metadata so the webhook can look it up:

```go
// PlaceReminderCall initiates an appointment reminder outbound call.
func PlaceReminderCall(patientPhone, appointmentID string) error {
    payload := map[string]any{
        "from":      os.Getenv("SAUTIKIT_FROM_NUMBER"),
        "to":        []string{patientPhone},
        "voice_callback_url": os.Getenv("BASE_URL") + "/webhooks/reminder-voice",
        "metadata":  map[string]string{"appointment_id": appointmentID},
    }
    body, _ := json.Marshal(payload)
    req, _ := http.NewRequest("POST", "https://api.sautikit.com/v1/calls", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("SAUTIKIT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return fmt.Errorf("placing reminder call: %w", err)
    }
    defer resp.Body.Close()
    if resp.StatusCode >= 300 {
        return fmt.Errorf("sautikit returned %d", resp.StatusCode)
    }
    return nil
}
```

The voice webhook handler in Go:

```go
// POST /webhooks/reminder-voice
func 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:

```go
// POST /webhooks/reminder-response
func 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)
}
```

## Patient Data Privacy Under DPA 2019

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:

1. **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.
2. **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.

## Pattern 2: Nurse Triage Line (IVR Symptom Screener)

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:

<!-- diagram omitted -->

The inbound call flow uses a series of `Say` + `GetDigits` pairs to collect symptom severity signals:

```json
{
  "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.

```json
{
  "actions": [
    {
      "say": {
        "text": "Connecting you to a nurse now. Please hold.",
        "language": "en-KE"
      }
    },
    {
      "dial": {
        "number": "+254711000001",
        "timeout": 30,
        "callerId": "+254711000000"
      }
    }
  ]
}
```

### Cost Model for a Triage Line

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.

## Pattern 3: Community Health Worker Referral Callbacks

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:

1. Places an outbound call to the referred patient
2. Confirms the appointment
3. Records the confirmation for the CHW's case file

```js
// 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:

```js
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`,
        },
      },
    ],
  });
});
```

## Cost Model for a 500-Bed Hospital Running Voice Reminders

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, 24 h retention on the free 1 GB tier) | ~11 700 min | n/a | KES 0 |
| **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](https://helloduty.com) is the in-family platform that adds SMS, WhatsApp, USSD, and ticketing.

## Get started

1. [Create a Sautikit workspace](/) and claim a phone number.
2. Top up over **M-Pesa**: KES billing, no card.
3. Schedule a morning cron that queries tomorrow's appointments and fires a reminder call for each patient.

**[Start with Sautikit →](/)** &nbsp;·&nbsp; **[See pricing →](/pricing)** &nbsp;·&nbsp; **[Need SMS, WhatsApp & an agent desk? Helloduty →](https://helloduty.com)**

## Getting Started

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](/developers/guides/quickstart-place-a-call) covers initial setup. The [voice-actions reference](/developers/concepts/voice-actions) documents `GetDigits`, `Say`, and `Redirect`. Recording storage tiers are at [/pricing](/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.
