---
title: 'Promise-to-pay by keypad: turn debt reminder calls into commitments with DTMF'
description: >-
  Turn debt reminder calls into commitments: capture promise-to-pay by keypad
  with Sautikit voice actions, track kept rates, and stay inside CBK rules.
summary: >-
  A guide for Kenyan collections teams: capture promise-to-pay with getDigits,
  dispatch on Digits, pair the commitment with M-Pesa paybill, log it for the
  follow-up call, and stay compliant.
date: 2026-07-28T00:00:00.000Z
type: blog
---


## Summary

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.

## Why "we informed them" is not an outcome

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](/blog/dtmf-detection-reliability).)

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](/blog/debt-reminder-robocall-mpesa-kenya). This post drills into the commitment layer: capturing the promise, measuring whether it is kept, and running the follow-up.

## The two numbers to track: PTP rate and kept rate

| Metric | Formula | What it tells you |
|---|---|---|
| **PTP rate** | commitments ÷ right-party contacts | 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.

## The flow: one handler, one menu, Digits dispatch

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](/developers/concepts/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:

```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 express from "express";
import { sauti } from "@sautikit/node";

const app = express();
app.use(express.json()); // voice callbacks arrive as JSON

const FROM_NUMBER = process.env.SAUTIKIT_FROM;   // your workspace-owned number
const AGENT_LINE = "+254720000100";              // your collections desk
const 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.

## Make the promise immediately payable

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](/blog/mpesa-daraja-stk-push-integration).)

**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](/blog/debt-reminder-robocall-mpesa-kenya).

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](https://helloduty.com), the multi-channel CX platform Sautikit is part of; Sautikit itself stays the voice leg.

## Log the promise, schedule the follow-up

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:

```js
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](https://sautikit.com/mcp).

## What a PTP call costs

Outbound calls bill at **KES 3/min, per second after connect** (as of 2026-06-30 — see [/pricing](/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.

## Compliance: the rules that actually apply

> 
> 
> Debt-collection calling in Kenya sits under the CBK's conduct rules and the Data Protection Act, with the ODPC as the most active enforcer. This section is guidance, not legal advice — confirm current obligations with the CBK, the ODPC, and the Communications Authority before you dial.
> 
> 

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

## 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 number's `voice_callback_url` at the PTP handler above and dial your first batch.

**[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 DSL, including `getDigits` and the Digits follow-up
- [getDigits verb parameters](/developers/voice-actions/get-digits)
- [Webhooks: events, signatures, and retries](/developers/concepts/webhooks) for tracking call outcomes at scale
