---
title: >-
  Take your voice stack across East Africa: what changes at the border, and what
  doesn't
description: >-
  Expanding from Kenya to Tanzania or Uganda? Your voice-action code crosses the
  border as-is. Numbers, currency, and regulators don't. The playbook.
summary: >-
  The East Africa expansion playbook for voice: what stays identical across
  borders (your code), what changes (numbers, currency, regulation, language),
  and how to build one stack for all of it.
date: 2026-10-20T00:00:00.000Z
type: blog
---


## Summary

Kenyan companies are expanding into Tanzania and Uganda faster than their phone systems are. The good news: if you built your voice stack on webhooks and voice-action documents, almost all of your code is already country-agnostic — what changes at the border is numbers, currency, regulation, and language, and every one of those is config, not a rewrite. This post is the playbook: what ports unchanged, what you localise per country, and how to structure one codebase that treats "market" as a column, not a fork.

## The code crosses the border unchanged

Start with an inventory of what you have actually built. For most teams running voice on Sautikit, it is three things:

**Voice-action documents.** Your IVR menus, reminder scripts, and agent-transfer flows are JSON documents — `say`, `getDigits`, `dial`, `record`, `hangup` (the [voice actions reference](/developers/concepts/voice-actions) has the full set). Nothing in that vocabulary knows what country it is running in. A payment-reminder flow that collects a keypress in Nairobi collects the same keypress in Dar es Salaam; only the prompt text and the caller ID differ.

**Webhook handlers.** Your signature verification, your `Digits` dispatch logic, your event dedupe on `X-Sautikit-Event-Id` — all of it is protocol, not geography. A handler that [verifies HMAC signatures over raw bytes](/developers/guides/verify-webhook-signatures) does exactly the same work whether the call behind the event connected in Kenya or anywhere else.

**Campaign and agent logic.** Retry ladders, promise-to-pay capture, escalation rules, idempotency keys like `reminder:{loan_id}:{due_date}` — business logic keyed on customers and accounts, not on country codes.

That inventory is the core of the expansion argument: the expensive part of a voice stack — the part you debugged at 2 a.m. — travels for free. What follows is the shorter list of things that genuinely change.

## What changes: numbers and caller ID

The single biggest operational difference between markets is the number on the caller's screen. A +254 caller ID dialing Ugandan customers reads as a foreign call, and foreign calls get answered less and trusted less. Real expansion means presenting a local number in each market — which makes numbers the one resource you must acquire per country.

Treat that as an onboarding task per market, not a code change. Every country has its own numbering plan, its own KYC requirements for acquiring numbers, and its own rules about what caller ID you may present. None of that belongs in your handlers. It belongs in a config table, keyed by country, holding exactly one thing your code reads: which number to dial from.

```bash
npm install @sautikit/node@0.2.0
```

```js
// config/markets.js — everything country-specific lives here, never in handlers.
import { SautikitClient } from "@sautikit/node";

const client = new SautikitClient({ apiKey: process.env.SAUTIKIT_API_KEY });

const MARKETS = {
  KE: {
    live: true,
    callerId: "+254700000001", // your Sautikit Nairobi DID
    currency: "KES",
    timezone: "Africa/Nairobi",
    callingWindow: { startHour: 8, endHour: 18 }, // your policy, applied everywhere
  },
  TZ: { live: false, currency: "TZS", timezone: "Africa/Dar_es_Salaam" },
  UG: { live: false, currency: "UGX", timezone: "Africa/Kampala" },
};

const marketFor = (msisdn) =>
  msisdn.startsWith("+254") ? MARKETS.KE
  : msisdn.startsWith("+255") ? MARKETS.TZ
  : msisdn.startsWith("+256") ? MARKETS.UG
  : null;

async function placeReminderCall(msisdn, loanId, dueDate) {
  const market = marketFor(msisdn);
  if (!market?.live) return queueForMarketLaunch(msisdn, loanId); // no fork, no crash

  return client.calls.create({
    from: market.callerId,
    to: [msisdn],
    idempotencyKey: `reminder:${loanId}:${dueDate}`,
  });
}
```

`@sautikit/node` is in preview — pin the version; the API may change before 1.0.

Notice the `live` flag. A stack built this way expands by flipping a boolean and filling in a caller ID — the day a new market opens, your first campaign there runs on code that has been in production for a year.

## What changes: currency and cost tracking

Sautikit bills in KES today, in minor units — `50000` means KES 500.00, on every wallet and rate field in the API. The expansion mistake is baking that assumption in as "divide by 100 and print KES."

Three rules make cost tracking multi-country-proof:

**Carry the currency with the amount.** Sautikit's API already does — number claims and wallet reads return a `currency` field alongside every `*_minor` amount. Store the pair, never the bare integer.

**Never hardcode the minor-unit exponent.** KES carries cents, but per ISO 4217 the Ugandan shilling is a zero-decimal currency. "Divide by 100" is a bug waiting for its first UGX row; keep an exponent per currency code instead.

**Report per-country, consolidate deliberately.** Cost-per-answered-call is only comparable within one market. Roll up across markets at an FX rate you choose and date — do not let an average silently mix currencies.

Rates themselves are per-market data, not constants: Kenya's current card is on [/pricing](/pricing), and per the [coverage page](/coverage), a Kenyan DID can place and receive calls to and from any country, billed in KES — so cross-border legs are possible today even before local numbers are.

## What changes: regulation (and why the Kenya playbook mostly ports)

If you built compliant calling in Kenya, you did the hard thinking already. Kenya's rules for automated and collections calling — the CBK Digital Credit Providers Regulations 2022 (Reg 20: no contacting a borrower's phone contacts, no threats, no shaming) plus the Data Protection Act 2019 (voice is personal data: notify at collection, play a recording notice, honour refusals, keep a documented retention schedule) — turn out to be a template the neighbours largely share.

| Rule | Kenya | Tanzania | Uganda |
|---|---|---|---|
| Abusive collection conduct banned | CBK DCP Regulations 2022, Reg 20 | Bank of Tanzania (Financial Consumer Protection) Regulations, 2019 — "abusive debt recovery" defined and banned as an unfair business practice | UMRA Digital Lending Guidelines 2024 — mirror Kenya's Reg 20 almost verbatim |
| Calling a borrower's contact list | Prohibited (Reg 20) | Agents barred from harassment, threats, intimidation, humiliation | Prohibited (no calls or messages to the customer's contact list) |
| Data protection regime | Data Protection Act 2019, ODPC enforcement | Personal Data Protection Act 2022 — register with the PDPC **before** processing | Data Protection and Privacy Act (Cap 97) |

Two country-specific escalations deserve a line each. Tanzania's Bank of Tanzania regulations (in force since 2019; verified against the BOT text as of July 2026) bar recovery agents from "threats, intimidation, humiliation, misrepresentation, deception" — and Tanzania's 2022 data law requires registering with the Personal Data Protection Commission before you process personal data, a checklist item with real lead time. Uganda raised the stakes differently: in 2025 a Ugandan court criminally convicted a lending-company director for debt-shaming a borrower (reported by the LEX Africa network, September 2025) — unauthorised use of personal data for debt recovery is an offence there, not just a fine.

One more portable insight: none of Kenya's collection rules sets a statutory calling-hours window (the oft-quoted 7 a.m.–7 p.m. is SMS-marketing guidance, as of mid-2026). So write a calling window into your own policy — the `callingWindow` in the config above — set it conservatively, and apply it in every market regardless of what local statute requires. Policy you control ports perfectly; statute-chasing does not.

> 
> 
> This section summarises instruments as researched in July 2026, and Kenya's DCP Regulations are slated to be replaced by the draft Non-Deposit Taking Credit Providers Regulations. Treat this as orientation, not legal advice — verify each market's current rules before you launch.
> 
> 

## Language: Swahili is your bridge, prompts are your config

East Africa hands Kenyan teams a genuine advantage: Swahili spans Kenya and Tanzania. A Swahili payment prompt written in Nairobi is intelligible in Dar es Salaam — but not identical in register. Tanzanian Swahili runs more formal and standard, while Kenyan phone Swahili freely mixes English. The practical rule: reuse your Swahili scripts as the starting point, then have a Tanzanian speaker review the register before launch. That is a copy edit, not a localisation project.

Uganda is a different map. English is the language of business and works as the IVR default; Luganda dominates the central region and earns its place as a first-menu language option, with other regional languages as your traffic justifies them.

Architecturally, treat prompt text exactly like caller IDs: per-market config. Your `say` verbs take a `text` and `language` field, so the flow logic stays shared while the words vary. And where you are not confident a synthetic voice renders a language convincingly, sidestep the question: record the prompt with a native speaker and serve it with the `play` verb instead. A recorded greeting is the most portable TTS strategy there is — it works identically in every market and every language.

```json
{
  "actions": [
    { "play": { "url": "https://cdn.example.com/prompts/tz/greeting-sw.mp3", "loop": 0 } },
    {
      "getDigits": {
        "timeout": 8,
        "numDigits": 1,
        "nested": [{ "play": { "url": "https://cdn.example.com/prompts/tz/menu-sw.mp3", "loop": 0 } }]
      }
    }
  ]
}
```

The handler serving this document is the same one serving your Kenyan traffic — it just reached into a different row of the prompt table. Since these are plain JSON documents, you can also validate each market's variants against the exact runtime schema before deploying, using the `validate_voice_actions` tool on the [Sautikit MCP server](/mcp) — no API call, no cost.

## The architecture in one sentence

One codebase; a market table holding caller IDs, currency, timezone, calling window, and prompts; a `marketFor()` lookup at every call-placing boundary; cost rows stored as `(amount_minor, currency)`; and a per-market `live` flag so unlaunched countries queue instead of erroring. Teams that structure this way expand by editing config. Teams that fork a "Tanzania version" of the repo spend the next two years merging bug fixes twice.

## Where Sautikit fits today

Plainly: Sautikit is live in Kenya today, with instant local numbers, per-second KES billing, and M-Pesa top-up. Uganda, Tanzania, and Rwanda — alongside Nigeria, Ghana, and South Africa — are queued markets with open waitlists on the [coverage page](/coverage), which is the single source of truth for what is live. Kenyan DIDs can already carry calls to and from any country, billed in KES, so your regional call flows can run on Kenya legs while local numbers are queued.

The playbook above is deliberately independent of that timeline. Build country-agnostic now, on the market that is live; join the waitlists for the markets on your roadmap; and if your rollout needs voice plus SMS, WhatsApp, and an agent desk across more African markets today, talk to the [Helloduty](https://helloduty.com) team — the multi-channel CX platform Sautikit is part of — about coverage.

## Get started

1. [Create a Sautikit workspace](/) and claim a Kenyan number.
2. Top up over **M-Pesa**: KES billing, no card.
3. Structure your handlers around a market config table, then join the [coverage waitlists](/coverage) for the countries on your roadmap.

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