Term 3 of the 2026 school year runs 24 August to 23 October, and school fees are due before children report. A Sautikit voice reminder reaches parents that a text does not: an outbound call in the parent's language with a press-1 acknowledgement menu, paired with the school's M-Pesa paybill, on three waves timed to the term calendar. For a 600-parent school the whole term costs about KES 5,100.
Most schools already send fee-reminder texts, and two weeks into term the same children are still being sent home. A text has three weaknesses that a call does not:
No attention. A text waits to be read. Parents answer a ringing phone that shows 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.
A voice call also works on every handset, with no smartphone, data bundle or app.
Per the Ministry of Education's 2026 school calendar, Term 3 runs Monday 24 August to Friday 23 October 2026. It is the shortest term of the year and ends in national assessments. Fees are due before reporting day, and children with balances are usually sent home in the first two weeks.
That calendar gives you three calling windows: the week before opening, reporting week, and a mid-term sweep before assessments begin.
The call shows the school's number. When the parent answers, they hear:
"Habari. This is a message from [your school's name]. 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 connects to the bursar's line, where a payment plan can be agreed.
Press 3: hear the paybill. The paybill number and account format are read out slowly, twice, with an option to repeat. This is the branch that leads to a payment.
You need a Sautikit workspace, a claimed number parents will learn to recognise, a wallet topped up by M-Pesa or card, 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:
npm install @sautikit/node@0.2.0
import { SautikitClient } from "@sautikit/node";const client = new SautikitClient({ apiKey: process.env.SAUTIKIT_API_KEY });const SCHOOL_NUMBER = "+254720000000"; // your Sautikit numberconst roster = await unpaidParents(); // [{ admissionNo, phone }] from your fee systemfor (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 matter here. The idempotencyKey is built from the term, the wave and the admission number, so re-running a crashed script replays the original response and never calls the same parent twice. And 20-second spacing means 600 dials take about 3 hours 20 minutes: one evening window, with only a few bursar transfers at any moment. Parents who do not answer cost you nothing (billing is per second after the call is answered) and go onto a retry list for the next evening.
Dev shops running this for several schools can run the whole thing from Claude (place the wave, watch call statuses, check the wallet) through the Sautikit MCP server.
getDigits does not take a follow-up URL. When the parent presses a key, Sautikit POSTs back to the samevoice_callback_url with the dtmfDigits field populated, and your handler dispatches on it. One route serves the entire flow: the menu when there are no digits, a branch when digits arrive.
import express from "express";import { sauti } from "@sautikit/node";const app = express();app.use(express.json()); // the voice callback delivers JSONconst BURSAR = "+254720000001";const SCHOOL_NAME = "your school's name";const menu = () => sauti .getDigits({ timeout: 7, numDigits: 1 }, (b) => // timeout is in seconds b.say( `Habari. This is a message from ${SCHOOL_NAME}. ` + "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. For the paybill number, 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.dtmfDigits ?? req.body.digits ?? ""; // Outbound reminder: the parent you dialled is clientDialedNumber. const msisdn = req.body.clientDialedNumber || ""; 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 with no .build() step.
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. There is no session state and no extra URL: the dtmfDigits dispatch does all the routing. If no key arrives within the timeout, the flow falls through to the polite goodbye.
The press-3 branch needs the most care, because the parent has to catch a number by ear, often somewhere noisy:
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 payment during the call can trigger an M-Pesa STK push, the press-1-to-pay pattern from our debt-reminder robocall guide, built on the 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 sent as a text after the call, SautiDesk sends SMS and WhatsApp from the same numbers.
Three waves cover Term 3 without calling any parent more than they need:
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 to Fri 4 Sep
Outstanding balance, press 2 for a payment plan
Mid-term
Week of 21 Sep
Final balances before assessments and the 23 Oct closing
Enforce these cadence rules in code:
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 covers the data-protection backdrop.
The say verb takes a language field. The examples above use en-KE, which handles the mixed English and Swahili most Kenyan school communication already uses. A few practices:
Name the school in the first sentence. An anonymous robocall about money gets hung up on.
Keep sentences short. Long sentences are harder to follow on a weak line. One fact per sentence.
Use each family's language. 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 covers timeout and numDigits tuning for Kenyan networks.
Sautikit pricing: outbound calls are KES 3.00 a minute (KES 0.05 a second, billed per second after the call is answered), incoming calls are free, unanswered calls cost nothing, and a Kenyan number is KES 500 a month. Current rates are on /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: if 70% of 600 parents answer, 420 answered × KES 2.25 = KES 945. The 180 unanswered calls cost nothing.
Retry evening: if 60% of the 180 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 KES 1,500 for three months of the number (3 × KES 500). That is about KES 5,100 for the whole of Term 3, with a record of 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 want to talk.
Claim the school's number for KES 500 a month at sautikit.com/numbers, point its voice_callback_url at the /voice/fees handler above, and schedule the 18 August wave.