M-PESA Africa Fintech Mobile Money STK Push

Why African Mobile Money Made Me a Better Payment Engineer

AL
Azim Litanga
· April 22, 2025 · ⏱ 11 min read
The PayCore Series — Article 4 of 4 · logicdynamics.net

Before I built PayCore, I thought I understood payment systems.

I'd integrated Stripe. I'd worked with PayPal. I understood OAuth flows, webhook signatures, idempotency keys, and the difference between authorization and capture. I thought that was the whole picture.

Then I integrated M-PESA. And I realized I had been looking at a corner of the map and calling it the world.

The scale that changes everything

M-PESA — Safaricom's mobile payment platform — launched in Kenya in 2007. By 2024:

51M
Active users
$61B
Annual volume (USD)
~50%
Of Kenya GDP flows through M-PESA
7
Countries served

M-PESA is not a fintech startup. It is financial infrastructure. And critically — it works on feature phones via USSD. No smartphone required.

This is the context that matters. M-PESA didn't win because it had a better app. It won because it solved financial inclusion for people who didn't have bank accounts, didn't have smartphones, and didn't have reliable internet — but did have a phone number.

"A phone number is an account. A PIN is authentication. A text message is a receipt. M-PESA understood this before fintech was a word." — PayCore Design Notes

What STK Push taught me about UX

Stripe's payment flow starts on your website. The customer enters card details into a form. Your server sends a request to Stripe. Stripe returns a result.

M-PESA's STK Push is fundamentally different. You don't bring the customer to the payment form — you push the payment form to the customer's phone:

The STK Push flow (what the customer experiences): 1. Customer clicks 'Pay with M-PESA' on your checkout 2. Your server calls Daraja API's STK Push endpoint 3. Customer's phone vibrates — USSD prompt appears: ┌─────────────────────────────────────┐ │ M-PESA │ │ Pay KES 500 to PayCore │ │ Account: PI_001A2B3C │ │ │ │ Enter M-PESA PIN: │ │ [____] │ │ │ │ [OK] [Cancel] │ └─────────────────────────────────────┘ 4. Customer enters their PIN 5. M-PESA confirms: 'KES 500 sent to PayCore. Ref: NLJ7RT61SV' 6. M-PESA POSTs callback to your server Total time: 15–30 seconds Internet required by customer: No Devices supported: Any phone with a SIM card

The insight: the best payment UX is the one that meets the customer where they already are. For 51 million Kenyans, that's a USSD prompt on their feature phone.

What Daraja taught me about API design

Stripe's API is a masterpiece of developer experience. Every endpoint is consistent. Every error is descriptive. The documentation has working examples for every language.

Daraja — M-PESA's API platform — is different. It works perfectly once you understand it. But it has quirks that will trip you up if you approach it expecting Stripe:

Daraja quirkWhat it teaches you
OAuth token endpoint is GET not POSTNever assume authentication conventions. Read the docs. Test early.
Password = Base64(ShortCode+Passkey+Timestamp)Security can be unconventional. Understand the threat model before judging the implementation.
Amounts must be integers — no decimalsCurrency representation is a design choice. Understand why before abstracting it away.
Phone must be 254XXXXXXXXX — no + no spacesReal users provide data in many formats. Normalization is not optional.
Webhook has no signature — IP whitelist insteadDifferent markets have different trust models. Build for the actual threat landscape.

What phone normalization taught me about edge cases

The NormalizePhone function in PayCore's MpesaAdapter is eight lines long. It handles five different phone number input formats. It is the most instructive eight lines of code I've written this year.

private static string NormalizePhone(string phone)
{
    phone = phone.Trim().Replace("+", "").Replace(" ", "");
    if (phone.StartsWith("0"))    phone = "254" + phone[1..];
    if (!phone.StartsWith("254")) phone = "254" + phone;
    return phone;
}

// What this handles:
// Local format:       0722000000    → 254722000000 ✅
// E.164:              +254722000000 → 254722000000 ✅
// Already correct:    254722000000  → 254722000000 ✅
// No prefix:          722000000     → 254722000000 ✅
// With spaces:        07 22 000 000 → 254722000000 ✅

Before writing this function, I would have said: "Phone number input. Easy. Just validate the format and reject anything invalid." After writing it, I understand that "invalid" is a developer's word, not a user's word. The user knows their phone number. They just don't know which format your API expects.

Normalization is respect. It meets the user where they are.

Building for the next billion

The next billion users of digital financial services are not in San Francisco or London. They're in Nairobi, Lagos, Kampala, Accra, and Dar es Salaam. They have phones — many of them feature phones. They have SIM cards. They have M-PESA or Airtel Money or MTN MoMo accounts.

They don't have Stripe accounts. They may not have bank accounts.

Building PayCore's M-PESA and Airtel adapters wasn't about adding feature flags to an existing Western payment system. It was about starting with a fundamentally different model of what a payment is — and building software that respects that difference.

"The developer who only knows Stripe knows one model of money movement. The developer who knows M-PESA knows the model that serves the majority of the world's population who need digital financial services most." — PayCore Design Philosophy

What I'd tell every payment developer

Integrate an African mobile money API. Not because your next project requires it. Because it will change how you think about:

That last one is the one that sticks with me most. M-PESA doesn't require the latest iPhone or a high-speed connection. It requires a SIM card and a PIN. That constraint — building for the constrained environment — produces better software than the absence of constraints ever will.


Where PayCore goes next

PayCore currently supports M-PESA (KES, TZS, GHS) and Airtel (UGX). The next expansion is MTN MoMo — 65 million users across Nigeria, Cameroon, Côte d'Ivoire, and 18 other countries. Adding it requires one new adapter class, one DI registration, and three routing table entries. The architecture was designed for exactly this.

If that's your business — reach out at logicdynamics.net.

"Building PayCore didn't make PayCore more complicated. It made it more honest about what a payment actually is." — Azim Litanga, LogicDynamics
← Previous
From Zero to Docker: Containerizing a .NET Payment Platform
End of The PayCore Series
AL
Azim Litanga

Founder of LogicDynamics — .NET engineer, builder, and creator. Passionate about Blazor, Web APIs, AI, and shipping real software. Based in Oklahoma City. Built in Oklahoma City. Running on four continents.