---
title: 'School fees season in Kenya: Term 3 voice reminders parents actually answer'
description: >-
  Term 3 opens Aug 24, 2026. Build a fee-reminder call wave with a press-1
  acknowledgement menu, pair it with your M-Pesa paybill, and cost it in KES.
summary: >-
  Why fee-reminder calls outperform SMS, a say + getDigits acknowledgement flow,
  M-Pesa paybill pairing, a three-wave Term 3 cadence, and per-second KES cost
  math for a 600-parent school.
date: 2026-08-11T00:00:00.000Z
type: blog
---


## Summary

Term 3 of the 2026 school year runs 24 August to 23 October, and fees are due before children report. This guide builds the fee-reminder system that actually moves the needle: an outbound call wave in the parent's language with a press-1 acknowledgement menu, wired to the school's M-Pesa paybill, on a three-wave cadence timed to the term calendar. It closes with per-second KES cost math for a 600-parent school, written for school administrators and the dev shops that serve them.

## Why the fee SMS gets ignored and a call gets answered

Every school already sends fee-reminder texts: the SMS lands between a betting promo and a loan offer, gets archived, and two weeks into term the same children are being sent home. Three structural problems, none of which more SMS fixes:

- **No attention.** A text is passive; it waits to be read. A ringing phone showing the school's own number is an event. Parents answer calls from their child's school.
- **No language fit.** A voice prompt can greet the parent in mixed English and Swahili and speak the paybill number aloud. An SMS assumes the parent reads texts, in your language, on that particular day.
- **No acknowledgement.** SMS gives you a delivery report at best. A call with a keypad menu gives you per-parent state: fees settled, wants the bursar, needed the paybill number read out. Your follow-up list shrinks after every wave instead of staying at 600 names.

And voice works on every handset: no smartphone, no data bundle, no app.

## The Term 3 calendar you are working against

Per the Ministry of Education's 2026 school calendar (released October 2025), Term 3 runs **Monday 24 August to Friday 23 October 2026** — the shortest, most exam-loaded term of the year, with national assessments starting 26 October and the KCSE window opening 19 October. Fees are due before reporting day, and fee send-homes historically peak around the second week of term, a perennial pattern the national dailies document at every opening.

The Ministry also confirmed in December 2025 that there is no school-fees increase for 2026, so the reminder can be short: parents already know the figures.

That calendar gives you three natural calling windows: the week before opening, the send-home peak in reporting week, and a mid-term sweep before assessments begin.

## The reminder call, from the parent's side

The parent's phone rings, caller ID showing the school's number. They pick up and hear:

> "Habari. This is a message from Makini Junior School. School fees for Term 3 are due before reporting day on Monday, 24th August. If fees are already settled, press 1. To speak to the bursar, press 2. Kusikia namba ya paybill ya shule, bonyeza tatu — to hear the school's M-Pesa paybill number, press 3."

Three branches, each with a clear job:

- **Press 1 — fees settled.** The parent self-clears. You mark them acknowledged and they drop out of the next wave.
- **Press 2 — speak to the bursar.** The call bridges to the bursar's line, where payment plans get agreed instead of children getting sent home.
- **Press 3 — hear the paybill.** The paybill number and account format are read out slowly, twice, with an option to repeat — the branch that converts a reminder into a payment.

<!-- diagram omitted -->

## Placing the wave

You need a Sautikit workspace, a claimed number parents will learn to recognise, an M-Pesa-topped wallet, and a fee roster with phone numbers and admission numbers. Point the number's routing at your handler first — `PUT /v1/numbers/{id}/routing` with `{ "voice_callback_url": "https://school.example.com/voice/fees" }` — then loop the roster:

```bash
npm install @sautikit/node@0.2.0
```

> 
> 
> `@sautikit/node` is in preview — pin the version; the API may change before 1.0.
> 
> 

```js
import { SautikitClient } from "@sautikit/node";

const client = new SautikitClient({ apiKey: process.env.SAUTIKIT_API_KEY });

const SCHOOL_NUMBER = "+254709000123"; // your Sautikit number

const roster = await unpaidParents(); // [{ admissionNo, phone }] from your fee system

for (const parent of roster) {
  const { call_id, status } = await client.calls.create({
    from: SCHOOL_NUMBER,
    to: [parent.phone],
    // Re-running this script can never double-dial a parent:
    idempotencyKey: `fees:term3-2026:wave1:${parent.admissionNo}`,
  });

  await recordAttempt(parent.admissionNo, call_id, status);

  // Pace the wave so parents pressing 2 do not stampede the bursar.
  await new Promise((r) => setTimeout(r, 20_000));
}
```

Two details carry the weight here. The `idempotencyKey` is built from business identity — term, wave, admission number — so a crashed script re-run replays the original response instead of calling the same parent twice. And 20-second spacing means 600 dials take about 3 hours 20 minutes: one evening window, with only a handful of concurrent bursar transfers at any moment. Parents who do not answer cost you nothing in talk time (billing is per second after connect) and go onto a retry list for the next evening.

Dev shops running this for several schools can drive the whole thing from Claude — place the wave, watch call statuses, check the wallet — through the [Sautikit MCP server](/mcp).

## One handler, dispatched on Digits

`getDigits` does not take a follow-up URL. When the parent presses a key, Sautikit POSTs back to the **same** `voice_callback_url` with the `Digits` field populated, and your handler dispatches on it. One route serves the entire flow — menu when there are no digits, branch when digits arrive:

```js
import express from "express";
import { sauti } from "@sautikit/node";

const app = express();
app.use(express.json()); // the voice callback delivers JSON

const BURSAR = "+254720000200";

const menu = () =>
  sauti
    .getDigits({ timeout: 7, numDigits: 1 }, (b) => // timeout is in seconds
      b.say(
        "Habari. This is a message from Makini Junior School. " +
          "School fees for Term 3 are due before reporting day on Monday, 24th August. " +
          "If fees are already settled, press 1. " +
          "To speak to the bursar, press 2. " +
          "Kusikia namba ya paybill ya shule, bonyeza tatu — press 3.",
        { language: "en-KE" },
      ),
    )
    .say("Asante kwa kusikiliza. Goodbye.", { language: "en-KE" })
    .hangup();

app.post("/voice/fees", async (req, res) => {
  const digits = req.body.Digits || "";
  // Outbound reminder: the parent is the dialed party.
  const msisdn = req.body.destinationNumber || req.body.callerNumber || "";

  if (!digits) {
    // First hit after answer: play the menu and collect one digit.
    return res.json(menu());
  }

  if (digits === "1") {
    await markAcknowledged(msisdn); // drop this parent from the next wave
    return res.json(
      sauti
        .say("Asante sana. We have noted that fees are settled. Kwaheri.", { language: "en-KE" })
        .hangup(),
    );
  }

  if (digits === "2") {
    await flagForBursar(msisdn); // so the bursar's screen shows who is calling
    return res.json(
      sauti
        .say("Connecting you to the bursar. Tafadhali subiri.", { language: "en-KE" })
        .dial({ number: BURSAR, timeout: 25 })
        .say("The bursar is not available right now. We will call you back. Kwaheri.", {
          language: "en-KE",
        })
        .hangup(),
    );
  }

  if (digits === "3") {
    return res.json(
      sauti
        .getDigits({ timeout: 6, numDigits: 1 }, (b) =>
          b.say(
            "Pay by M-Pesa paybill. Namba ya paybill ni: 8, 8, 4, 4, 5, 5. " +
              "The account number is your child's admission number. " +
              "Narudia: paybill 8, 8, 4, 4, 5, 5. Account: admission number. " +
              "To hear this again, press 3. If fees are settled, press 1.",
            { language: "en-KE" },
          ),
        )
        .say("Asante. Kwaheri.", { language: "en-KE" })
        .hangup(),
    );
  }

  // Unrecognised key: replay the menu.
  return res.json(menu());
});
```

`res.json()` calls the chain's `toJSON()`, so the builder serializes straight to the
`{ actions: [...] }` envelope — no `.build()` step needed.

Because the paybill branch ends in another `getDigits`, pressing 3 again loops the read-out, and pressing 1 from inside it still lands on the acknowledgement branch. No session state, no extra URLs — the `Digits` dispatch does all the routing. If no key arrives within the timeout, the flow falls through to the polite goodbye.

## Pairing with the school's M-Pesa paybill

The press-3 branch is where design earns its keep, because the parent is standing in a matatu trying to memorise a number:

- **Read digits individually, with pauses.** "8, 8, 4, 4, 5, 5" survives a noisy line; "eight hundred eighty-four thousand" does not.
- **Repeat the whole instruction twice**, and offer press-3-to-repeat. The second reading is the one parents transcribe.
- **Make the account number the admission number.** Parents know it, and it is your reconciliation key: every row on the paybill statement carries it, so a nightly job can mark those parents paid and shrink the next wave automatically.

Schools that want tighter closure can trigger an M-Pesa STK push during the call — the press-1-to-pay pattern from our [debt-reminder robocall guide](/blog/debt-reminder-robocall-mpesa-kenya), built on the [Daraja STK push integration](/blog/mpesa-daraja-stk-push-integration). For most schools, though, paybill-plus-admission-number is already familiar to parents and needs no new payment plumbing.

If you also want the paybill details to land as a text after the call, [Helloduty](https://helloduty.com) — the multi-channel CX platform Sautikit is part of — adds SMS and WhatsApp on top of the same voice layer.

## Cadence: three waves, not thirty

The goal is to be the school that reminded helpfully, not the school that robocalled nightly. Three waves cover Term 3:

| Wave | When | Message focus |
|---|---|---|
| Before-term | Tue 18 Aug (the week before the 24 Aug opening) | Amount due, paybill details, reporting date |
| Reporting week | Mon 31 Aug – Fri 4 Sep (the send-home peak) | Outstanding balance, press 2 for a payment plan |
| Mid-term | Week of 21 Sep | Final balances before assessments and the 23 Oct closing |

Cadence rules worth enforcing in code, not policy documents:

- **One call per wave, one retry** the following evening for non-answers.
- **Call in the early evening**, roughly 6 to 8 p.m. on weekdays, when parents are off work. Kenya has no statutory calling-hours law for this; treat daytime-to-early-evening as courtesy.
- **Suppress acknowledged and paid parents** before every wave. The press-1 data and the paybill reconciliation both feed this.
- **Honour opt-outs.** Collect phone numbers through the admission pack for school communications, and remove anyone who asks. Our [Kenya voice regulation guide](/blog/voice-regulation-kenya) covers the data-protection backdrop.

## Multilingual TTS that parents understand

The `say` verb takes a `language` field — the examples above use `en-KE`, which handles the mixed English-Swahili register most Kenyan school communication already uses. A few practices from the field:

- **Name the school in the first sentence.** An anonymous robocall about money gets hung up on.
- **Keep sentences short.** TTS prosody degrades on long clauses, and parents on 2G links lose syllables. One fact per sentence.
- **Match the roster, not the region.** If your fee system stores a per-family language preference, use it to select a full-Swahili script (`sw-KE`) rather than assuming one language fits all 600.
- **Test the menu on real handsets.** Keypad tones vary across carriers and codecs; our [DTMF reliability guide](/blog/dtmf-detection-reliability) covers timeout and `numDigits` tuning for Kenyan networks.

## What this costs a 600-parent school

Sautikit pricing (as of 2026-06-30): outbound calls at KES 3 per minute — **KES 0.05 per second, billed per second after connect** — inbound free, and a local Nairobi number at KES 100 per month excluding VAT (KES 116 including VAT). Current rates are always on [/pricing](/pricing).

The reminder above runs about 30 seconds of menu plus a short branch, so budget a 45-second average answered call:

- 45 seconds × KES 0.05 = **KES 2.25 per answered call**
- Wave 1: 600 dials at a 70% answer rate → 420 answered × KES 2.25 = **KES 945**. The 180 unanswered dials accrue no talk-time charges.
- Retry evening: 180 dials, 60% answer → 108 × KES 2.25 = **KES 243**
- **Wave total: about KES 1,188** to reach 528 of 600 parents (88%)

Three waves across the term come to roughly **3 × KES 1,188 = KES 3,564**, plus about KES 348 for three months of the number (3 × KES 116). Call it **under KES 4,000 for the whole of Term 3** — with an acknowledgement trail showing exactly who heard the message and what they pressed.

The manual alternative: a bursar phoning 600 parents at two minutes each is 20 hours of calling per wave, before airtime, with no log. The automated wave finishes in one evening and hands the bursar only the press-2 list — the parents who actually want to talk.

## Get started

1. [Create a Sautikit workspace](/) and claim a phone number.
2. Top up over **M-Pesa**: KES billing, no card.
3. Point your school number's `voice_callback_url` at the `/voice/fees` handler above and schedule the 18 August wave.

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

## Further reading

- [Voice actions reference](/developers/concepts/voice-actions): the full JSON verb vocabulary, including `say`, `getDigits`, `dial`, and `hangup`
- [getDigits verb reference](/developers/voice-actions/get-digits): parameters, timeout semantics, and the Digits follow-up webhook
- [Claim and route a number](/developers/guides/claim-and-route-a-number): claiming a school number and setting `voice_callback_url`
- [Bulk voice campaigns in Kenya](/blog/bulk-voice-call-api-kenya): scaling the same pattern beyond one school
