How M-Pesa C2B Reconciliation Actually Fails in Production

S
Samuel Kimani
May 11, 2026 3 min read

The STK Push tutorial ends where production begins

Every M-Pesa integration starts with STK Push because Safaricom's docs lead with it. STK is clean: you push, the user pays, you get a callback. C2B is where the assumptions you built start failing, and where we've burned the most engineering hours over the last three years.

C2B is the flow you reach for when a customer paying via a Paybill or Till on their own. There's no app context, no CheckoutRequestID handed back to you. Just a callback from Safaricom telling you somebody paid you, sometime, with a reference string the customer typed in themselves.

Failure mode 1: late callbacks

Safaricom's C2B confirmation URL is best-effort. We've seen confirmations arrive 4 hours after the payment, and we've seen them never arrive. If your reconciliation logic assumes "no callback = no payment", you've already lost. The customer paid, your system says they didn't, your support inbox fills up.The fix is a reconciliation worker that polls Safaricom's account statement endpoint on a schedule (we run it every 15 minutes) and matches transactions by MpesaReceiptNumber. If the receipt exists in your statement but not in your payments table, you missed a callback. Backfill it, fire the downstream side-effects, and log a "callback miss" metric so you can track Safaricom's reliability.

Failure mode 2: BillRef mismatches

The BillRefNumber is whatever the customer typed when they paid. They will get it wrong. They will type their phone number. They will type the account number from your competitor. They will paste an old reference from a screenshot. We've seen all of these.Build a BillRef parser that tries multiple matchers in order: exact match on invoice number, exact match on customer code, fuzzy match by phone number (the MSISDN is in the callback), then a manual review queue for everything else. Around 3% of C2B payments in our systems land in the review queue; that's the number to track.

Failure mode 3: duplicate confirmations

Safaricom retries failed confirmations. If your confirmation URL returns anything other than the exact JSON {"ResultCode":0,"ResultDesc":"Accepted"} within their timeout, you'll get the same payment delivered again. And again. We've seen the same MpesaReceiptNumber arrive four times in a row.Idempotency is non-negotiable. Use the MpesaReceiptNumber as a unique key in your payments table with a database-level constraint, and let the second insert fail loudly. Don't try to be clever with "if exists then skip", concurrent callbacks race each other and you'll get duplicates anyway.

The dashboard you actually need

Three numbers tell you whether your reconciliation is healthy: callback miss rate (caught by the polling worker), BillRef review queue depth, and duplicate confirmation count. We surface all three on a Grafana panel and alert when miss rate exceeds 1% over an hour. Anything below 0.5% is steady state for Kenyan production; above 2% and Safaricom is probably having an incident they haven't announced yet.

Need software built?

Tell us what you need. We respond within 24 hours with a realistic quote.