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.
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 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 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.
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.
npm install @sautikit/node@0.2.0
// 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.
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, and per the coverage page, 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.
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.
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.
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 — no API call, no cost.
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.
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, 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 team — the multi-channel CX platform Sautikit is part of — about coverage.