---
title: >-
  Cut fraud and disputes in Kenyan fintech: voice OTP, loan confirmation, and
  recorded evidence calls
description: >-
  Three high-ROI voice patterns for Kenyan fintech: OTP for large transactions,
  loan disbursement calls, and recorded dispute evidence.
summary: >-
  Kenyan fintech teams get the most from voice in three places: OTP on
  high-value transactions, loan disbursement confirmation, and recorded dispute
  resolution calls.
date: 2026-07-03T00:00:00.000Z
type: blog
---


## Summary

Kenyan fintech is built on mobile, but the mobile experience breaks at the edges: failed SMS OTPs, disputed loan terms, and escalations that drown chat queues. This post documents the three highest-ROI voice integrations for Kenyan fintech teams and the specific Sautikit patterns that power each, including cost calculations per 10 000 monthly active borrowers.

## The Kenyan Fintech Landscape: Where Voice Fits

Kenya's fintech ecosystem operates across a density gradient that has no equivalent elsewhere in the world. At the top, commercial banks like KCB and Equity push multi-million-shilling transactions through USSD and mobile apps to customers on Safaricom handsets. Below them sit SACCOs, digital lenders, and mobile-money aggregators that serve borrowers in peri-urban areas where the most reliable channel is still a voice call.

Three failure modes keep recurring across this landscape:

1. **SMS OTP delivery drops below 80% during peak hours.** Safaricom's SMS delivery rates are high on average, but SIM swaps, MSISDN porting, and transient tower congestion create silent OTP failures at exactly the wrong moment: when a borrower is about to authorise a high-value payment.
2. **Loan term disputes are expensive to resolve without evidence.** When a borrower says "I was never told the interest rate was 8% per month", a fintech with no recorded call has no auditable evidence. The dispute goes to a human reviewer, and the cost is borne by the platform.
3. **Customer service escalations are slow and expensive.** A 10-person contact centre in Nairobi handling 400 calls/day at KES 50 000/agent/month costs roughly KES 31.25 per resolved call. Many of those calls could have been deflected with a well-structured voice flow.

Voice does not solve all of these, but it addresses each one precisely, at a cost that is reasonable at scale.

## Pattern 1: Voice OTP for High-Value Transactions

### The regulatory context

The Central Bank of Kenya's framework for digital credit providers and mobile banking imposes a practical requirement: for transactions above KES 50 000, a second authentication factor beyond SMS OTP is required. The multi-channel requirement means the second factor must travel on a different channel from the first. Voice satisfies this cleanly without requiring a hardware token.

### The implementation

The flow is straightforward: generate a cryptographically random OTP, initiate an outbound call, read the digits individually via `Say`, and collect the user's input via `GetDigits`.

```javascript
const crypto = require("crypto");

// Generate a 6-digit OTP from a cryptographically secure source
function generateOtp() {
  // Use rejection sampling to avoid modulo bias
  let val;
  do {
    val = crypto.randomBytes(4).readUInt32BE(0);
  } while (val >= 4000000000); // above 4e9 would create bias mod 1000000
  return String(val % 1000000).padStart(6, "0");
}

async function initiateVoiceOtp(toNumber, otp) {
  const res = await fetch("https://api.sautikit.com/v1/calls", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: process.env.SAUTIKIT_FROM_NUMBER,
      to: [toNumber],
      voice_callback_url: `${process.env.BASE_URL}/webhooks/otp-voice`,
    }),
  });
  return res.json();
}
```

The voice webhook handler returns a `Say` + `GetDigits` action sequence:

```javascript
// POST /webhooks/otp-voice
app.post("/webhooks/otp-voice", (req, res) => {
  const otp = getOtpForSession(req.body.sessionId); // your lookup
  // Spell each digit individually: "4 8 2 1" not "four thousand eight hundred..."
  const spokenOtp = otp.split("").join(". ");

  res.json({
    actions: [
      {
        say: {
          text: `Your verification code is: ${spokenOtp}. I repeat: ${spokenOtp}. Please enter the code now.`,
          language: "en-KE",
          loop: 1,
        },
      },
      {
        getDigits: {
          numDigits: 6,
          timeout: 10000,
          finishOnKey: "#",
          action: `${process.env.BASE_URL}/webhooks/otp-verify`,
        },
      },
    ],
  });
});
```

Reading digits individually matters. On a 12.2 kbps AMR-NB path (Safaricom 2G/3G), the word "eight" is more intelligible than the spoken number "4821" because short phoneme sequences survive codec compression better than multi-syllable number strings. The post [DTMF detection reliability on Kenyan networks](/blog/dtmf-detection-reliability) covers the underlying audio path in detail.

### What the `GetDigits` verification webhook receives

```json
{
  "callSessionState": "Answered",
  "sessionId": "HD_9d2b1f53-8c0e-4f1d-9a6b",
  "digits": "482163",
  "finishOn": "numDigits"
}
```

The `digits` field contains exactly what the user pressed. Your handler compares it to the stored OTP and either confirms the transaction or plays an error prompt.

## Pattern 2: Loan Disbursement Confirmation Calls

### The regulatory basis

The CBK Prudential Guidelines for Digital Credit Providers (CBK/PG/15) require that digital lenders disclose loan terms to borrowers in a verifiable format before disbursement. A recorded voice call, with the borrower's verbal acceptance captured via the `Record` verb, satisfies this requirement and creates an auditable log that is retrievable via Sautikit's recording API.

### The voice flow

The disbursement call reads back the principal, interest rate, repayment schedule, and total cost. At the end, the borrower presses 1 to accept or 2 to decline.

```javascript
// POST /webhooks/disbursement-voice
app.post("/webhooks/disbursement-voice", async (req, res) => {
  const loan = await getLoanBySession(req.body.sessionId);

  res.json({
    actions: [
      {
        record: {
          action: `${process.env.BASE_URL}/webhooks/disbursement-recorded`,
          maxLength: 120,
          playBeep: false,
        },
      },
      {
        say: {
          text: `Hello ${loan.borrowerName}. Your loan of K E S ${loan.principalFormatted} has been approved. The interest rate is ${loan.ratePercent} percent per month. Total repayable is K E S ${loan.totalRepayableFormatted} over ${loan.termMonths} months. Press 1 to accept these terms and receive your funds. Press 2 to decline.`,
          language: "en-KE",
        },
      },
      {
        getDigits: {
          numDigits: 1,
          timeout: 15000,
          finishOnKey: "",
          action: `${process.env.BASE_URL}/webhooks/disbursement-accept`,
        },
      },
    ],
  });
});
```

The `Record` verb is placed first in the actions array so the entire call, including the terms disclosure and the borrower's verbal response, is captured from the start. The recording URL is returned in the `disbursement-recorded` webhook:

```json
{
  "call_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",
  "recording_url": "https://recordings.sautikit.com/9d2b1f53.wav?sig=...",
  "duration_seconds": 68
}
```

Store the `recording_url` (or better, the `call_id` and fetch a fresh signed URL on demand) against the loan record in your database. If a borrower disputes the terms, the recording is the evidence.

### Recording storage cost at scale

Recording bills at KES 0.50/min, and storage is tiered: 1 GB is included free with a 24-hour retention window, and paid tiers add capacity and longer retention — 10 GB (3-day retention) at KES 599/month, 40 GB (14 days) at KES 1,999, 100 GB (30 days) at KES 4,999. Ten thousand monthly borrowers at one 45-second confirmation call each equals 7 500 minutes of audio per month, which at 64 kbps mono WAV is approximately 3.5 GB — the 10 GB tier at KES 599/month covers it. Because dispute evidence usually needs to outlive any tier's retention window, archive dispute-relevant recordings to your own storage (fetch the signed URL and copy the audio when the call completes); the Sautikit tier acts as the capture buffer.

## Pattern 3: Dispute Resolution with Recorded Voice Evidence

### The failure mode without recording

A borrower contacts your support team claiming the interest rate is different from what they agreed to. Without a recorded call, you have your system's log showing what was presented on-screen. The borrower has their recollection. This is a dispute that takes hours to resolve and frequently ends in a write-off or a concession to avoid the escalation cost.

With a recorded disbursement call, the support agent retrieves the recording, plays the relevant segment, and the dispute resolves in under five minutes.

### Fetching a recording for a specific loan

```bash
# Retrieve a fresh signed URL for the recording
curl -s "https://api.sautikit.com/v1/calls/${CALL_ID}/recording" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"
```

```json
{
  "call_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",
  "status": "ready",
  "url": "https://recordings.sautikit.com/9d2b1f53.wav?sig=abc123&expires=1754524800",
  "mime": "audio/wav",
  "duration_ms": 68000,
  "expires_at": "2026-08-06T11:00:00Z"
}
```

The signed URL expires in 15 minutes. Your support interface should fetch a fresh URL each time an agent opens the dispute screen. Do not cache the URL in your database; cache the `call_id` and re-fetch on demand.

### Data retention under DPA 2019

The Kenya Data Protection Act 2019 requires that personal data not be retained longer than necessary. For voice recordings that serve as dispute evidence, a 90-day retention window is reasonable and defensible. On-platform retention follows your storage tier (24 hours on the free 1 GB tier up to 30 days on the 100 GB tier; change tiers via `PUT /v1/storage/tier`), so for a 90-day evidence window, copy recordings you may need into your own archive before the tier window lapses. Recordings past the platform retention window return `status: expired` and HTTP 410.

## Cost Model per 10 000 Monthly Active Borrowers

Here is the full cost breakdown for a digital lender running all three voice patterns:

| Pattern | Calls/month | Avg duration | Cost per call (KES 3/min outbound) | Monthly total |
|---|---|---|---|---|
| Voice OTP (high-value only, 20% of users) | 2 000 | 25 sec | 1.25 | KES 2 500 |
| Disbursement confirmation | 10 000 | 45 sec | 2.25 | KES 22 500 |
| Dispute callbacks (5% escalation rate) | 500 | 3 min | 9.00 | KES 4 500 |
| Recording storage (10 GB tier) | n/a | 7 500 min | KES 599/month flat | KES 599 |
| **Total** | | | | **KES 30 099** |

Per-borrower voice cost: KES 3.01/month. For a lender earning KES 500–2 000/borrower/month in interest, this is well under 1% of revenue.

Compare this to the cost of running the equivalent human process: a team of two customer service agents handling OTP resets and dispute calls costs approximately KES 120 000/month in salary alone, before office costs or management overhead.

## Integration Effort: Estimated Days to Production

| Pattern | Backend complexity | Time to production |
|---|---|---|
| Voice OTP | Low (outbound call + webhook handler) | 1–2 days |
| Disbursement confirmation | Medium (recording + acceptance flow) | 2–3 days |
| Dispute recording retrieval | Low (API call + signed URL) | 0.5 days |

All three patterns share the same Sautikit API key and wallet. No additional accounts or agreements are needed.

If your OTP, disbursement, and dispute flows also need SMS fallback, WhatsApp confirmations, or a human-agent desk for escalations, [Helloduty](https://helloduty.com) adds those channels alongside Sautikit voice.

## Get started

1. [Create a Sautikit workspace](/) and claim a phone number.
2. Top up over **M-Pesa**: KES billing, no card.
3. Place your first outbound confirmation call and capture the borrower's verbal acceptance with the `record` action.

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

## Getting Started

The [quickstart guide](/developers/guides/quickstart-place-a-call) covers placing your first outbound call in under 15 minutes. From there, the [voice-actions reference](/developers/concepts/voice-actions) documents every verb used in this post. Pricing details, including the recording storage tiers, are at [/pricing](/pricing).

For fintech teams integrating with Safaricom's Daraja API alongside Sautikit, the [M-Pesa payment confirmation call guide](/blog/mpesa-payment-confirmation-call) covers the normalisation of Daraja phone number formats and the concurrency queue pattern for month-end payment peaks.
