A reminder call that only informs is wasted airtime. This guide turns the debt reminder into a commitment device: the borrower presses 1 to confirm they will pay by Friday, 2 to talk to an agent, 3 to request a payment plan — and your system logs a promise-to-pay (PTP) it can measure and follow up on. We build the complete Sautikit voice-actions flow with the Digits-dispatch pattern, pair the commitment with an M-Pesa paybill so settling is immediate, and close with the rules that actually govern collection calls in Kenya.
Most automated reminder calls end the same way: the balance is read out, the borrower says nothing, the call hangs up, and the CRM records "contacted". That row is almost useless. You paid for airtime and learned nothing about whether money is coming.
Collections practice has a better unit of progress: the promise-to-pay. Instead of ending the call after the reminder, you ask for a dated commitment on the spot. A captured PTP does three things a plain reminder cannot:
It converts an ambiguous contact into a dated, auditable outcome. "Confirmed payment by Friday 31 July, 14:02, key press 1, session HD_..." is evidence; "call completed" is not.
It segments the book automatically. Committers, agent-requests, plan-requests, and silent hang-ups each get a different next action — no analyst required.
It gives the follow-up call a script. "On Tuesday you confirmed you would pay by Friday" is a very different conversation from a cold re-dial.
The keypad is the right capture channel for this. DTMF works on every handset from a feature phone up, needs no speech recognition, and produces an unambiguous answer. (If your key presses are getting lost on some carriers, see our note on reliable DTMF detection on Kenyan networks.)
We covered the reminder robocall itself — including firing an M-Pesa STK push mid-call when the borrower presses to pay now — in Collect debts faster in Kenya. This post drills into the commitment layer: capturing the promise, measuring whether it is kept, and running the follow-up.
How persuasive the call is. A right-party contact means you reached the borrower, not voicemail or a relative.
Kept rate
amount received on/before the promise date ÷ amount promised
Whether the promises are real. This is the number that pays salaries.
Collections-KPI benchmarks published by contact-centre vendors (as of 2026) put first-contact PTP rates above 50% for strong operations. Treat that as directional: we found no published Kenya-specific PTP benchmarks as of July 2026, so your first campaign's job is to establish your own baseline, then improve it.
The two metrics discipline each other. A script that pressures everyone into pressing 1 will show a beautiful PTP rate and a terrible kept rate — you manufactured promises, not payments. That is exactly why the menu below offers an agent and a payment plan alongside the commitment: a borrower who genuinely cannot pay by Friday needs a real alternative, and a plan kept beats a promise broken.
On Sautikit, call control lives on the phone number: set the number's voice_callback_url once with PUT /v1/numbers/{id}/routing, and the platform POSTs the call state to that URL on every step. You respond with a JSON list of voice actions.
The part people get wrong: getDigits does not take a follow-up URL. When the borrower presses a key, the platform POSTs back to your same voice_callback_url with the Digits field populated. One handler therefore serves the whole flow — it plays the menu when Digits is empty and dispatches on the value when it arrives:
Build the actions with the sauti builder from the Node SDK rather than hand-writing the JSON — same envelope, but the verb options are typed:
npm install @sautikit/node@0.2.0
import express from "express";import { sauti } from "@sautikit/node";const app = express();app.use(express.json()); // voice callbacks arrive as JSONconst FROM_NUMBER = process.env.SAUTIKIT_FROM; // your workspace-owned numberconst AGENT_LINE = "+254720000100"; // your collections deskconst PAYBILL = "8 8 1 1 0 0"; // spaced so TTS reads it digit by digit// One-time setup: PUT /v1/numbers/{id}/routing// { "voice_callback_url": "https://collections.example.co.ke/voice/ptp" }app.post("/voice/ptp", async (req, res) => { const digits = req.body.Digits || ""; // Outbound reminder: the borrower is the dialed party. const msisdn = req.body.clientDialedNumber || ""; // the number you dialed; destinationNumber is your own number in both directions const loan = await loanByMsisdn(msisdn); // your CRM lookup if (!digits) { // First hit (call answered): state the debt, then ask for a commitment. return res.json( sauti .say( "Hello. This is a payment reminder from Acme Credit. " + `Your balance of ${loan.balance_kes} shillings was due on ${loan.due_human}.`, { language: "en-KE" }, ) // timeout is in SECONDS; the nested prompt plays while the platform listens. .getDigits({ timeout: 8, numDigits: 1 }, [ { say: { text: "To confirm you will pay by Friday, press 1. " + "Kuthibitisha utalipa ifikapo Ijumaa, bonyeza moja. " + "To speak to an agent, press 2. Kuongea na wakala, bonyeza mbili. " + "For a payment plan, press 3. Kwa mpango wa malipo, bonyeza tatu.", language: "en-KE", }, }, ]) .say("We did not receive a selection. Goodbye. Kwaheri.", { language: "en-KE" }) .hangup(), ); } // Follow-up hit: the platform POSTed back with Digits populated. if (digits === "1") { const promisedAt = nextFriday(); // e.g. "2026-07-31" await logPtp(loan, { promised_at: promisedAt, channel: "dtmf" }); return res.json( sauti .say( "Thank you. We have recorded your commitment to pay by Friday. " + `You can pay any time before then on paybill ${PAYBILL}, ` + `account number ${loan.loan_id}. ` + `Lipa kupitia paybill ${PAYBILL}, akaunti ${loan.loan_id}. Goodbye.`, { language: "en-KE" }, ) .hangup(), ); } if (digits === "2") { await logOutcome(loan, "agent_requested"); return res.json(sauti.dial({ number: AGENT_LINE, callerId: FROM_NUMBER, timeout: 30 })); } if (digits === "3") { await logOutcome(loan, "plan_requested"); return res.json( sauti .say( "Thank you. An agent will call you within one business day " + "to set up a payment plan. Goodbye. Kwaheri.", { language: "en-KE" }, ) .hangup(), ); } // Unrecognised digit: replay the menu. return res.json(sauti.redirect("https://collections.example.co.ke/voice/ptp"));});
Script rules worth keeping: identify the lender in the first sentence (transparency, and a Reg 20 requirement in spirit — see the compliance section), state the amount and due date plainly, lead in English and repeat the menu in Swahili, and keep the whole thing under 40 seconds. The timeout on getDigits is in seconds, and the nested say plays while the platform listens, so the borrower can press mid-prompt.
A commitment decays fast if paying requires effort later. Two ways to close the gap:
Read out the paybill on the spot. The press-1 confirmation above voices the lender's M-Pesa paybill and the loan ID as the account reference, so the borrower can settle the moment the call ends — or any day before Friday, in full or in part. Wire your Daraja C2B confirmation webhook to the PTP record and the promise marks itself kept the second money lands. (Setting up Daraja? See our STK push integration guide.)
Or collect during the call. If you would rather not wait for Friday at all, add a "press 4 to pay now" branch that fires an STK push to the same handset mid-call — that pattern, end to end, is in the press-1-to-pay robocall guide.
A follow-up SMS with the paybill details helps the borrower who was walking down a noisy street when the call came — that is a job for Helloduty, the multi-channel CX platform Sautikit is part of; Sautikit itself stays the voice leg.
A PTP is only worth what your follow-up makes of it. Store, at minimum: loan_id, the borrower's MSISDN, the promise date, when it was captured, the call's session ID, the digit pressed, and the eventual outcome (kept, partial, broken). That last field is your kept-rate numerator.
Then run a daily job: everyone whose promise expires today and has not paid gets one follow-up call, placed through the same POST /v1/calls you used for the reminder:
import { SautikitClient } from "@sautikit/node";const client = new SautikitClient({ apiKey: process.env.SAUTIKIT_API_KEY });// Run each morning: dial unpaid promises falling due today.async function dialPtpFollowUps(today) { for (const ptp of await unpaidPromises(today)) { await client.calls.create({ from: FROM_NUMBER, to: [ptp.msisdn], // One follow-up per promise, however often the job re-runs. idempotencyKey: `ptp-followup:${ptp.loan_id}:${ptp.promised_at}`, }); }}
The idempotency key keyed on loan_id + promised_at means a crashed-and-restarted job never double-dials a borrower — which is not just polite; repeated calls are the fastest way to trip the harassment rules below. The follow-up script should open by referencing the commitment ("On Tuesday you confirmed you would pay by Friday; we have not yet received a payment") and offer the same three keys.
If your team works out of chat, this whole loop — pull yesterday's outcomes, dial the follow-up batch, check wallet balance — can also run from Claude via the hosted Sautikit MCP server.
Outbound calls bill at KES 3/min, per second after connect (as of 2026-06-30 — see /pricing for current rates). A reminder-plus-commitment call runs about 35 seconds:
35 seconds × (KES 3 / 60) ≈ KES 1.75 per answered call
A 10,000-borrower cycle costs roughly KES 17,500 in call spend and comes back with a dated commitment or an explicit escalation for every answered call — data no SMS blast produces at any price.
Never call the borrower's contacts. The CBK Digital Credit Providers Regulations, 2022 (Regulation 20) prohibit, in the course of debt collection, accessing a customer's phone book, making unsolicited calls or messages to the customer's contacts, threats, obscene language, and public shaming — plus a catch-all against any conduct that harasses, oppresses, or abuses (text verified as of July 2026). Enforcement is real: the ODPC fined Whitepath KES 5,000,000 in April 2023 over contact-list harvesting and third-party messages. Dial the borrower, and only the borrower.
Frequency is conduct too. Reg 20's catch-all means excessive call frequency can itself be harassment. Cap attempts per day and per week in code, not in policy documents.
Calling hours are best practice, not statute. Kenya has no statutory time-of-day limit for collection calls (verified against the DCP Regulations text, as of July 2026). The commonly cited 7 a.m.–7 p.m. window comes from the Communications Authority's Consumer Protection Guidelines (§6.2.2.5) and is written for SMS marketing. Keeping voice reminders inside business hours is still the right call — it protects your kept rate as much as your compliance posture.
If you record, notify first. A recorded voice is personal data under the Data Protection Act, 2019, and section 29 requires informing the person at the point of collection. In early 2026 the ODPC awarded KES 700,000 against Liquid Telecommunications Kenya after a call was recorded despite an explicit refusal (Complaint No. 1125 of 2025). If you record the commitment for evidence, put the notice in the opening say — and honour a no.
The regime is being renamed. CBK's draft Non-Deposit Taking Credit Providers Regulations, 2025 (published August 2025) will replace the DCP Regulations and extend the same conduct rules to all credit-only lenders; as of April 2026 licensing still runs under the 2022 framework. If you lend, watch for gazettement.