Why STK push is harder than it looks
Daraja makes it look like a three-step flow: request, callback, done. In production the failure modes pile up — duplicate callbacks, missing callbacks, users who close the SIM Toolkit, network blips, and reconciliation gaps that only show up at month-end.
After running STK in production for HyipX, JetX, AviatorMode, and several SME tills, here are the five patterns we reach for every single time.
1. Always treat the callback as untrusted
Daraja will retry. Sometimes it'll deliver the same callback twice in 30 seconds, sometimes the same callback three days later when their network catches up. Your endpoint must be idempotent.
We key every transaction by Daraja's MerchantRequestID and CheckoutRequestID and store them with a unique index. Second time the same ID arrives, we no-op and return 200 OK.
2. Verify before crediting
Just because the callback says ResultCode=0 doesn't mean it really succeeded. We've seen forged callbacks in the wild. Every time a callback says success, we call the Transaction Status API with the receipt number and only credit when Safaricom confirms the M-Pesa receipt.
3. Reconciliation is not optional
Run a nightly job that pulls the C2B confirmation feed and compares it to your internal ledger. The first time we skipped this we missed three deposits worth ~80,000 KES.
4. Status polling for the user, not the system
The user staring at the phone wants feedback. We poll the transaction status every 3 seconds for up to 60 seconds on the frontend, but the backend never trusts that — the source of truth is always the callback plus the verification call.
5. Save the raw payload
Store the full Daraja JSON in a payment_callbacks table before parsing. When something breaks at 2am you'll want the raw bytes, not your interpreted version.
Wrapping up
STK push isn't hard once you accept that the wire is hostile. Idempotency, verification, reconciliation, raw logging — that's 90% of the work. Get those right and you'll sleep at night.
If you're integrating M-Pesa and want a second pair of eyes, we maintain a Laravel package that bakes these patterns in. Get in touch.