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:
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.
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 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 quirk | What it teaches you |
|---|---|
| OAuth token endpoint is GET not POST | Never 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 decimals | Currency representation is a design choice. Understand why before abstracting it away. |
| Phone must be 254XXXXXXXXX — no + no spaces | Real users provide data in many formats. Normalization is not optional. |
| Webhook has no signature — IP whitelist instead | Different 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.
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.
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:
- Authentication — you'll learn to not assume OAuth looks one specific way
- Amount representation — you'll learn why storing money as integers matters
- Input normalization — you'll learn to meet users where they are, not where you want them to be
- UX assumptions — you'll learn that the best interface isn't a form on a webpage — it's a push notification on the device the user already trusts
- Scale — you'll learn that $61 billion moves through a system that works on a 2007 Nokia
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.