Build · KE · UG · RW · TZ

One checkout for Kenya, Uganda, Rwanda and Tanzania

One payment flow across four East African markets: phone prefixes, networks, currency decimals and settlement, step by step, without four integrations.

Ultraner team··5 min read

East Africa looks like one market from a pitch deck and four markets from a payments log.

The East African Community has spent two decades building a common market 3, and the GSMA's reports consistently show East Africa carrying the largest share of mobile money value on the continent 1. A company in Nairobi selling to Kampala, Kigali and Dar es Salaam is a natural expansion. But the payer in each of those cities uses a different set of wallets, a different currency with different decimal rules, and a different phone number format. One payment flow across all four is very achievable. It just has to be built on purpose.

Here is the order we would do it in.

Step 1: Write down the four markets as data, not as if-statements

Before any code, put the facts in one table your whole team can see.

KenyaUgandaRwandaTanzania
CurrencyKESUGXRWFTZS
Decimal places in API amounts2000
Phone prefix254256250255
Total digits12121212
Networks in checkoutM-Pesa (Safaricom), Airtel Money, T-KashMTN MoMo, Airtel MoneyMTN MoMo, Airtel MoneyM-Pesa (Vodacom), Airtel Money, Mixx by Yas, Halopesa, TTCL T-Pesa, AzamPesa
Wallet payoutsComingComingComingLive

Two lines in that table cause most of the bugs. The decimal row, and the payouts row.

Ugandan shillings, Rwandan francs and Tanzanian shillings are sent to the API as whole numbers. The Kenyan shilling has two decimal places under ISO 4217 2, so KES amounts go in cents. If your code multiplies by 100 everywhere, Ugandan customers get a prompt for a hundred times the price. If it multiplies nowhere, Kenyan customers get a prompt for a hundredth. Read the decimal places from the currency, every time.

The country pages hold the current version of every row: Kenya, Uganda, Rwanda, Tanzania.

Step 2: Let the phone number choose the country

The customer already told you their country when they typed their number. Use it.

Normalise the number to digits only with the country code (no +, no spaces), then read the first three digits:

  • 254 means Kenya, charge in KES.
  • 256 means Uganda, charge in UGX.
  • 250 means Rwanda, charge in RWF.
  • 255 means Tanzania, charge in TZS.

Ultraner's universal charge endpoint does the same thing on its side: it reads the prefix and routes to whichever rail serves that network in that country. In Tanzania several rails run side by side with automatic fallback; in Rwanda there is a primary and a fallback; Kenya and Uganda go through PawaPay. You pick the network. Ultraner picks the gateway, and your request body is identical across all four.

POST /v0/payments/charge
{ "method": "mobile_money", "amount": 35000, "phone": "256772123456", "provider": "MTN" }

That is UGX 35,000 to an MTN MoMo customer in Uganda. Change the phone to 250788123456 and the amount to 5000 and it is RWF 5,000 in Kigali. Same code path.

Step 3: Show only the networks that work for that country

Once you know the country, the network picker should show that country's wallets and nothing else. A Rwandan customer offered T-Kash will either be confused or try it and fail.

Do not hard-code the list in your frontend. Call GET /v5/providers, which returns the networks actually live for your account, with their limits and current status, and filter by country. Then pre-select a network using GET /v5/providers/predict?msisdn=... and let the customer change it. The prediction is a guess: people keep their numbers when they switch networks, and charging the wrong network fails the payment.

Step 4: Price in the customer's currency

There are two honest ways to price across four currencies.

Local price lists. You set KES 1,500, UGX 40,000, RWF 15,000 and TZS 28,000 for the same plan, each a round number that looks natural to a local buyer. More work, better conversion.

One base price, converted at checkout. You price in one currency and let Ultraner's currency conversion show the payer their own currency, crediting you in yours. Less work, and the amounts will not be round.

Most teams start with the second and move their top products to the first once they see which markets buy. Whatever you do, do not show a Ugandan customer a price in Kenyan shillings and expect them to do the arithmetic.

Step 5: Treat pending as pending, in every market

The response to a mobile money charge is always pending. The customer is approving on their phone. Outcomes arrive on your webhook as payment.success or payment.failed.

The timing varies by network, and some networks never report a declined or ignored prompt. Ultraner closes those as failed after 10 minutes. Your webhook handler must accept a payment.success that arrives after a payment.failed for the same transaction_id, because occasionally the network confirms late. Build that once and it works for all four countries.

Step 6: Know what each market costs

Fees are not uniform across East Africa. The rail serving most markets outside Tanzania charges 1% of its own plus the network's charge, and Kenya, Uganda and Zambia price in tiers rather than a flat percentage. In Tanzania and Uganda the payer is also charged a fee by their own network on top of what you collect, which never touches Ultraner but is worth telling your customer about.

Before you set prices, pull real fee quotes for your common basket sizes in each country. A margin that works on a TZS 50,000 order may not work on a UGX 5,000 one.

Step 7: Decide where the four currencies end up

Four currencies coming in raises the question of where they go. You have two options, and you can mix them:

  • Settle to your bank account. Settlement works to your own bank account in any country, with the FX spread shown separately when your account currency differs from your balance.
  • Pay out to wallets. Live in Tanzania today, so a Tanzanian refund or commission can go straight to a phone. For the other three, settle and pay onward until those rails are live.

Most teams keep one operating balance and one bank account, and convert once at settlement rather than four times.

Step 8: Test each country in the sandbox

With a uk_test_ key, every rail has a sandbox counterpart and no real gateway is ever touched. Outcomes are keyed off the end of the phone number, so you can test each country with its own prefix:

  • 254700000001 succeeds in Kenya.
  • 256700000002 fails with insufficient funds in Uganda.
  • 250700000003 times out in Rwanda and fails after about 10 minutes.
  • 255700000001 succeeds in Tanzania, so you can check the whole-number TZS path end to end.

Run each of those through your real checkout, read the screens your customer would see, and fix anything that reads as "something went wrong". The docs list every sandbox outcome.

What you end up with

One endpoint, one webhook handler, one ledger, and a small per-country table that tells your code which currency, which decimals and which networks apply. When you add a fifth market, you add a row, not a rewrite. That is the whole point of doing the first four properly.

Sources

  1. 1State of the Industry Report on Mobile Money, GSMA
  2. 2ISO 4217 Currency codes, ISO
  3. 3East African Community: Common Market, East African Community

For AI agents and tools: this article is also available as plain Markdown, every article is in the RSS feed, and the site guide for AI is at llms.txt.

Keep reading