---
title: 'Collect debts faster in Kenya: press-1-to-pay reminder robocalls over M-Pesa'
description: >-
  Automate debt collection in Kenya with Sautikit: a reminder robocall with
  press-1-to-pay via M-Pesa, promise-to-pay capture, and compliant calling.
summary: >-
  A step-by-step guide for collections, SACCO, and lending teams in Kenya: place
  reminder calls, let borrowers press 1 to pay via M-Pesa, capture
  promise-to-pay, and stay compliant.
date: 2026-07-01T00:00:00.000Z
type: blog
---


Collections teams in Kenya lose most of their recovery margin to two things: the cost of human agents dialing one number at a time, and borrowers who never see an SMS buried under fifty others. A **debt-reminder robocall** fixes both. One campaign dials your whole defaulter list, plays a bilingual message, and lets the borrower **press 1 to pay there and then via M-Pesa**: no agent, no app, no callback.

This guide builds that flow end to end on [Sautikit](/), Kenya's voice-only API: the call, the press-to-pay branch wired to M-Pesa STK push, promise-to-pay capture, an escalation ladder, and, critically, the **CBK and Data Protection Act guardrails** a Kenyan collections team must respect. Every code block runs against the real `/v1/calls` API.

> **TL;DR**
> - A robocall + `getDigits` "press 1 to pay" branch can trigger an M-Pesa STK push mid-call, so the borrower repays without hanging up.
> - At KES 3/min outbound, billed per second from the moment the call connects, a 25-second reminder call costs about **KES 1.25**, a fraction of an agent dial.
> - In Kenya, debt-collection **harassment became a criminal offence on 1 January 2025**; contacting a borrower's phone contacts is prohibited under the CBK Digital Credit Providers Regulations. Build consent and quiet-hours into the campaign, not as an afterthought.

## Why voice beats SMS for collections in Kenya

SMS is cheap, but for recovery it is weak: it competes with promotional clutter, it is easy to ignore, and it cannot *close*: the borrower still has to leave the inbox, open M-Pesa, and remember your paybill. A voice call does three things SMS cannot:

- **It commands attention.** A ringing phone gets answered; a text gets archived. Voice reaches feature-phone and low-literacy borrowers that app-based nudges miss entirely.
- **It closes in-session.** Press 1, and an STK push lands on the same handset while the borrower is still engaged. The decision and the payment happen in one interaction.
- **It creates an audit trail.** A recorded, timestamped interaction is exactly the kind of evidence a CBK-licensed lender wants on file.

This is why voice is a recovery channel, not just a notification channel. SMS still has a place (for the soft, early-DPD nudge), and that is where [Helloduty](https://helloduty.com), the multi-channel CX platform Sautikit is part of, fits: SMS and WhatsApp for the gentle reminders, Sautikit voice for the calls that actually collect. More on that ladder below.

## Step 1: Place the reminder call

A robocall campaign is just a loop over your defaulter list, one `POST /v1/calls` per borrower. The call is dialed immediately; the response returns before the borrower answers, so you poll `GET /v1/calls/{id}` or subscribe to the `call.answered` / `call.completed` webhooks for the outcome.

```js
// campaign.js: dial one borrower
const SAUTIKIT_API_KEY = process.env.SAUTIKIT_API_KEY;
const FROM_NUMBER = process.env.SAUTIKIT_FROM; // a number your workspace owns
const BASE = "https://api.sautikit.com";

async function placeReminder(borrower) {
  const resp = await fetch(`${BASE}/v1/calls`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${SAUTIKIT_API_KEY}`,
      "Content-Type": "application/json",
      // Reuse the loan ID so a retry never double-dials the same borrower
      "Idempotency-Key": `reminder:${borrower.loan_id}:${borrower.due_date}`,
    },
    body: JSON.stringify({ from: FROM_NUMBER, to: [borrower.msisdn] }),
  });
  if (resp.status === 402) {
    throw new Error("Wallet empty. Top up before running the campaign");
  }
  if (!resp.ok) throw new Error(`Call failed: ${resp.status}`);
  const { call_id } = await resp.json();
  return call_id;
}
```

The `Idempotency-Key` header matters in collections: campaigns get retried, lists get re-uploaded, and double-dialing a borrower is both annoying and, if it looks like harassment, a compliance risk. Keying on `loan_id + due_date` guarantees one reminder per cycle.

## Step 2: Voice the message (bilingual, short, transparent)

When the borrower answers, Sautikit POSTs the call state to your `voice_callback_url`. You respond with a list of **voice actions**. For a reminder, that is a `say` (the message) followed by a `getDigits` (the menu).

Three rules for the script: **identify the lender** (transparency is a legal requirement, not a courtesy), **state the amount and due date plainly**, and **keep it under 30 seconds**. Lead in English, repeat the menu in Swahili.

```js
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));

app.post("/voice/reminder", (req, res) => {
  const name = req.query.name || "";
  const amount = req.query.amount || "";  // e.g. "2,500"
  const due = req.query.due || "";        // e.g. "5 July"
  res.json({ actions: [
    { say: {
        text:
          `Hello ${name}. This is a payment reminder from Acme Credit. ` +
          `Your balance of ${amount} shillings is due on ${due}. ` +
          "To pay now by M-Pesa, press 1. " +
          "Kulipa sasa kwa M-Pesa, bonyeza moja. " +
          "To request more time, press 2. Kuomba muda zaidi, bonyeza mbili. " +
          "To speak to an agent, press 3. Kuongea na wakala, bonyeza tatu.",
        language: "en-KE",
    }},
    { getDigits: {
        numDigits: 1,
        timeout: 8000,
        finishOnKey: "",
        action: `${BASE_URL}/voice/reminder-branch?loan_id=${req.query.loan_id}`,
    }},
    // Fallback if the borrower says nothing
    { say: { text: "We did not receive a selection. Goodbye. Kwaheri.", language: "en-KE" }},
    { hangup: {} },
  ]});
});
```

If you are coming from an XML-based voice API (Africa's Talking, Twilio/TwiML), the same reminder flow reads naturally as XML. Both forms below work at runtime: Sautikit parses and validates the JSON DSL, and raw XML is forwarded to the telephony engine byte-for-byte, so your existing flows carry over as-is.

```xml
<Response>
  <Say language="en-KE">Hello. This is a payment reminder from Acme Credit. Your balance of 2,500 shillings is due on 5 July.</Say>
  <GetDigits maxDigits="1" timeout="8" action="https://yourapp.example/voice/reminder-branch">
    <Say>To pay now by M-Pesa, press 1. Kulipa sasa kwa M-Pesa, bonyeza moja.</Say>
  </GetDigits>
  <Say language="en-KE">We did not receive a selection. Goodbye. Kwaheri.</Say>
  <Hangup />
</Response>
```

```json
{
  "actions": [
    { "say": { "text": "Hello. This is a payment reminder from Acme Credit. Your balance of 2,500 shillings is due on 5 July.", "language": "en-KE" } },
    { "getDigits": {
        "numDigits": 1,
        "timeout": 8,
        "action": "https://yourapp.example/voice/reminder-branch",
        "nested": [ { "say": { "text": "To pay now by M-Pesa, press 1. Kulipa sasa kwa M-Pesa, bonyeza moja." } } ]
    }},
    { "say": { "text": "We did not receive a selection. Goodbye. Kwaheri.", "language": "en-KE" } },
    { "hangup": {} }
  ]
}
```

Note the two XML verb differences if you are porting muscle memory: the menu prompt `<Say>` nests inside `<GetDigits>`, and XML uses `maxDigits` where JSON uses `numDigits`.

## Step 3: Press 1 to pay, triggering an M-Pesa STK push mid-call

This is the part that turns a reminder into a collection. When the borrower presses 1, your branch handler fires a **Daraja STK push** to the same MSISDN, so the M-Pesa PIN prompt appears on their handset while they are still on the call. Respond with JSON actions, or return raw XML if you kept your AT/Twilio-style responses; both tabs below are the same handler:

**JSON actions**

```javascript
app.post("/voice/reminder-branch", async (req, res) => {
  const digit = req.body.Digits || "";
  const caller = req.body.From || "";
  const loan = await lookupLoan(req.query.loan_id);

  if (digit === "1") {
    // Fire the STK push (Safaricom Daraja) to the borrower's handset
    await stkPush({
      phone: caller,
      amount: loan.balance_kes,
      accountRef: loan.loan_id,
      description: "Loan repayment",
    });
    return res.json({ actions: [
      { say: {
          text:
            "Thank you. An M-Pesa request has been sent to your phone. " +
            "Tafadhali weka PIN yako kukamilisha malipo. Goodbye.",
          language: "en-KE",
      }},
      { hangup: {} },
    ]});
  }
  if (digit === "2") {
    return res.json({ actions: [{ redirect: { url: `/voice/promise-to-pay?loan_id=${loan.loan_id}` }}]});
  }
  if (digit === "3") {
    return res.json({ actions: [{ redirect: { url: `/voice/agent-transfer?loan_id=${loan.loan_id}` }}]});
  }
  // Replay the menu
  const qs = new URLSearchParams(req.query).toString();
  return res.json({ actions: [{ redirect: { url: `/voice/reminder?${qs}` }}]});
});
```

**XML response**

```javascript
app.post("/voice/reminder-branch", async (req, res) => {
  const digit = req.body.Digits || "";
  const caller = req.body.From || "";
  const loan = await lookupLoan(req.query.loan_id);
  const xml = (body) => res.type("application/xml").send(body);

  if (digit === "1") {
    // Fire the STK push (Safaricom Daraja) to the borrower's handset
    await stkPush({
      phone: caller,
      amount: loan.balance_kes,
      accountRef: loan.loan_id,
      description: "Loan repayment",
    });
    return xml(`<Response>
  <Say language="en-KE">Thank you. An M-Pesa request has been sent to your phone. Tafadhali weka PIN yako kukamilisha malipo. Goodbye.</Say>
  <Hangup />
</Response>`);
  }
  if (digit === "2") {
    return xml(`<Response><Redirect method="POST">/voice/promise-to-pay?loan_id=${loan.loan_id}</Redirect></Response>`);
  }
  if (digit === "3") {
    return xml(`<Response><Redirect method="POST">/voice/agent-transfer?loan_id=${loan.loan_id}</Redirect></Response>`);
  }
  // Replay the menu
  const qs = new URLSearchParams(req.query).toString();
  return xml(`<Response><Redirect method="POST">/voice/reminder?${qs}</Redirect></Response>`);
});
```

You do not need to invent `stk_push`; it is the standard Daraja `CustomerPayBillOnline` / Lipa na M-Pesa Online call. If you already top up your Sautikit wallet over M-Pesa, you have the same Daraja credentials in hand. (For the wallet side, see [the topups flow](/developers/api#wallet); for the voice verbs, the [voice actions reference](/developers/concepts/voice-actions).)

## Step 4: Capture promise-to-pay when they press 2

Not everyone can pay today. Press 2 records a **promise-to-pay (PTP)**, a date the borrower commits to, and schedules an automated follow-up call for that day. A captured PTP is far more recoverable than a silent miss.

```js
app.post("/voice/promise-to-pay", (req, res) => {
  res.json({ actions: [
    { say: {
        text: "On what date will you pay? Press the day of the month, then hash.",
        language: "en-KE",
    }},
    { getDigits: {
        numDigits: 2,
        finishOnKey: "#",
        timeout: 10000,
        action: `${BASE_URL}/voice/ptp-confirm?loan_id=${req.query.loan_id}`,
    }},
  ]});
});
```

Store the PTP keyed to the loan, then enqueue a follow-up call for that date using the same `placeReminder` loop. This is also the moment to record the call (a `record` action) so you keep an audit trail of the commitment.

## Step 5: The escalation ladder

A robocall is one rung. The recovery sequence that works in Kenya looks like this:

| Days past due | Channel | Action |
|---|---|---|
| 1–7 | SMS / WhatsApp (Helloduty) | Soft reminder, balance + due date |
| 8–30 | **Sautikit voice robocall** | Press-1-to-pay via M-Pesa, PTP capture |
| 31–60 | Voice + WhatsApp + agent dial | Personalised follow-up, payment plan |
| 60+ | Human recovery desk | Negotiated settlement, field follow-up |

Sautikit owns the voice rungs. The early SMS nudge and the late agent-desk / predictive-dialer rungs are exactly what [Helloduty](https://helloduty.com) adds: one platform for the whole ladder, with the recordings and audit trail CBK examiners expect. Start with the voice robocall; graduate to the full ladder when your book grows.

## What this costs

Outbound calls bill at **KES 3/min, billed per second from the moment the call connects**. A typical reminder call (greeting, amount, menu, press-1 acknowledgement) runs about 25 seconds:

```
25 seconds × (KES 3 / 60) ≈ KES 1.25 per answered call
```

For a book of 5,000 reminders a month, that is roughly **KES 6,250** in call spend, less than a single collections agent's daily output, and it runs in minutes. See [current KES rates and the billing increment](/pricing) for the authoritative numbers.

## Compliance: read this before you dial

> 
> 
> Debt collection in Kenya is regulated, and the rules tightened recently. This section is guidance, not legal advice. Confirm current obligations with the Communications Authority (CA), the Office of the Data Protection Commissioner (ODPC), and, if you are a licensed lender, the CBK before running a campaign.
> 
> 

- **Harassment is now criminal.** The Business Laws (Amendment) Act 2024, in force since **1 January 2025**, makes debt-collection harassment a criminal offence. Excessive call frequency, threats, or abusive language can expose your business to prosecution.
- **Never call the borrower's contacts.** The CBK Digital Credit Providers Regulations 2022 prohibit accessing a borrower's phonebook or contacting third parties about the debt. Dial the borrower, and only the borrower.
- **You need a lawful basis.** Reminder calls to your own borrowers can rest on contract / legitimate interest under the Data Protection Act 2019; you do not necessarily need marketing-style consent to remind a customer of their own debt. But you must honour objections and keep records. The ODPC has fined firms up to **KES 5 million** for misuse of personal data in collections.
- **Suppress Do-Not-Call numbers** and keep your calling to reasonable daytime hours. Kenya's published clock-hour windows are written around SMS; treating **08:00–18:00** as a safe ceiling for voice is best practice, not settled law.
- **Keep recordings and consent records.** They are your defence if a borrower complains.

For a deeper treatment, see our note on [voice and the Kenyan regulatory landscape](/developers/concepts/voice-actions).

## FAQ

### Can a robocall really take an M-Pesa payment during the call?
Yes. When the borrower presses 1, your webhook fires a Daraja STK push to the same phone number. The M-Pesa PIN prompt appears on their handset while they are still on the line, so they confirm without hanging up or opening an app.

### Is it legal to call borrowers about a debt in Kenya?
Reminding your own borrower of their own debt is generally lawful under contract / legitimate interest in the Data Protection Act 2019. What is prohibited is harassment, calling third parties or the borrower's contacts, and abusive language; since January 2025 those can be criminal offences.

### How much does a debt-reminder call cost?
About KES 1.25 for a 25-second call, at KES 3/min billed per second from the moment the call connects. A 5,000-borrower monthly campaign costs roughly KES 6,250 in call spend.

### Do I need a separate SMS provider for the early reminders?
Sautikit is voice-only by design. For SMS and WhatsApp rungs of the recovery ladder, Helloduty (the platform Sautikit is part of) covers those channels, so you can run the whole sequence from one account.

### What happens if the borrower doesn't answer?
The call returns a `no-answer` or `busy` status via the `call.completed` webhook. Retry on those statuses with bounded backoff (never retry an answered call), and respect a per-day attempt cap to stay clear of harassment rules.

## Get started

1. [Create a Sautikit workspace](/) and claim a phone number.
2. Top up over **M-Pesa**: KES billing, no card.
3. Point a `voice_callback_url` at the handlers above and dial your first borrower.

**[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): `Say`, `GetDigits`, `Dial`, `Record`, `Redirect`, `Hangup`
- [Calls API reference](/developers/api#calls): call status, events, and CDR fields
- [Pricing: KES rates and billing details](/pricing)
- [Safaricom Daraja STK Push (Lipa na M-Pesa Online)](https://developer.safaricom.co.ke/APIs/MpesaExpressSimulate): the payment request the press-1 branch fires
