DEV Community

Cover image for I've Integrated M-Pesa Into 6 Production Apps. Here's What the Docs Don't Warn You About
Sir. Brian
Sir. Brian

Posted on

I've Integrated M-Pesa Into 6 Production Apps. Here's What the Docs Don't Warn You About

Every payment integration tutorial assumes the same thing. Stripe, sandbox keys, instant webhooks, clean docs.

M-Pesa is the payment rail over 30 million people in Kenya use every day. I've now wired it into six different production apps. The official docs will get you a working sandbox demo in an afternoon. They will not prepare you for what actually breaks in production.

Here's what nobody tells you upfront.

The sandbox lies to you

Sandbox callbacks are fast and predictable. Production callbacks are not. I've seen STK push callbacks arrive 40 seconds after the user already gave up and closed the tab. Your UI has to account for this. Don't build a payment flow that assumes a callback within 3 seconds, because in production, it sometimes just doesn't work that way.

Localhost breaks the whole flow

Callback URLs have to be publicly reachable. That means no localhost testing, ever, without a tunnel. I use ngrok for local dev now by default, on every M-Pesa project, before I write a single line of integration code. Skipping this step costs you a full afternoon of confused debugging the first time.

Phone number formatting will bite you

Users type their number in every format imaginable. 07XX, +2547XX, 2547XX, sometimes with spaces. If you don't normalize this before it hits the API, you get silent failures that look like your integration is broken when it's actually just string formatting.

function normalizeMsisdn(input) {
  const digits = input.replace(/\D/g, '');
  if (digits.startsWith('254')) return digits;
  if (digits.startsWith('0')) return '254' + digits.slice(1);
  if (digits.startsWith('7') || digits.startsWith('1')) return '254' + digits;
  return digits;
}
Enter fullscreen mode Exit fullscreen mode

Small function. Saves you hours.

Idempotency is on you, not the API

Users double tap the pay button. Always. If you're not deduplicating requests on your end using a unique reference per transaction attempt, you will eventually charge someone twice. This isn't an edge case, it happens weekly at any real scale.

The real lesson

Most payment integration content online is written for Stripe and Silicon Valley assumptions. Fast callbacks, clean sandbox parity, predictable network conditions. Building for African markets means building for the opposite of all three. The fix isn't cleverness, it's defensive UI and idempotent backend logic from day one.

Curious if anyone else here has integrated payment rails outside the usual Stripe and PayPal defaults. What broke for you that the docs never mentioned?

Top comments (0)