STK Push is the friendly part of an M-Pesa integration. Your server makes one request, the customer's phone buzzes with a PIN prompt, and a few seconds later you hear back. When it works, it feels like magic.
Most of the work is in the other times. The prompt that never shows up. The customer who wanders off. The callback that arrives twice, or not at all. If you only build the happy path, your support inbox will build the rest for you.
One note before we start. This is about patterns, not exact field names. Safaricom's Daraja documentation is the source of truth for request formats, result codes and limits, and it changes, so read it alongside this.
Start by listing how it goes wrong
Before you write any code, write down what can happen after you ask for a payment:
- The customer ignores the prompt, cancels it, or types the wrong PIN.
- Their phone is off or out of coverage, so the prompt is late or never arrives.
- Your callback URL is unreachable for a minute because you were deploying.
- The callback reaches you twice.
- Your request goes out but the response gets lost, and now you don't know whether it was accepted.
- They pay, then close the tab.
Every item on that list needs an outcome you chose on purpose, not one you discover in production.
Treat a payment as something that moves through states
Give each payment a status, and save it before you call the API. Something like created, requested, pending, and then one of succeeded, failed or expired. Add the one people tend to skip: unknown. If you really can't tell whether money moved, admit it, and settle it later with a query or at reconciliation. A system with an honest "unknown" is easier to run than one that guesses.
Keep your own reference next to the CheckoutRequestID that Daraja gives you. That pair is how you find a payment again when someone phones to say, "I paid but nothing happened."
And one rule worth writing on the wall: the browser never decides that a payment succeeded. Only a verified callback, or your own status query, can do that.
Make the callback handler boring
Assume every callback can arrive twice, late, or out of order, and make that harmless. Look the payment up by CheckoutRequestID. If it's already final, say thanks and stop. Check the amount and reference against what you expected, and if they differ, hold the payment for a person instead of crediting it. Save the raw payload, because one day you'll want it. Reply quickly, and do the slow work (receipts, stock, notifications) afterwards.
Don't wait for a callback that isn't coming
Callbacks get lost. So for any payment that's still pending, ask Daraja yourself with the STK Push query endpoint: after a short wait, then again with longer gaps. When your window runs out, mark the payment expired, but keep it easy to find. If the customer turns out to have been charged, you'll want that record.
Be straight with the person on the other end
Someone staring at a spinner while they hunt for their PIN needs plain words. Tell them to check their phone. If you haven't heard back, tell them that too, and that they won't be charged twice. An "I've paid" button should run a status query, not start a second charge.
Guard against double requests on the server as well. If there's already a pending payment for that order and phone number, return it instead of starting another. When it's confirmed, show the M-Pesa receipt number. People like having it.
Reconcile daily, even when everything looks fine
Compare your records with the transactions M-Pesa reports for your shortcode, matching on the receipt number. You're hunting for two things: money that arrived but that you never marked as paid, and payments you marked as paid with nothing behind them. Automate the matching and leave a short list for a person to clear. Reconciliation is also the best bug detector you have for everything above.
Test the ugly cases
The sandbox rarely misbehaves the way production does, so write a small fake Daraja for your tests. Have it drop a callback, send one twice, delay one by five minutes, deliver things out of order, and report the wrong amount. If your handler shrugs at all of that, a busy Friday won't scare you.
Look after the keys
Keep the consumer key, secret and passkey on the server. Serve the callback over HTTPS on a URL that's hard to guess, and don't trust the payload blindly. For larger amounts, confirm with a status query before you hand over goods. Log enough to answer "what happened to this payment?" without hoarding personal data.
If you're building for trade, agriculture or public services, sketch these states and screens before you touch the integration. They decide whether support gets a handful of cases or a daily pile. And because payments so often happen on shaky connections, offline-first patterns for field apps pair well with all of this.