M-PESA Express is asynchronous: the request is initiated first, and the final result is returned later to the callback URL supplied by the merchant.
Store the request identifiers before waiting for the callback
Safaricom returns identifiers such as MerchantRequestID and CheckoutRequestID when an M-PESA Express request is accepted.
Persist them on the payment attempt immediately. The callback needs to map back to that attempt even if the original web request is long gone.
What the callback changes
The callback carries the final result code and description. On successful payments it also includes payment metadata such as the M-PESA receipt number and amount.
The handler should preserve the raw payload, map the result into an internal state and close the invoice only if the payment evidence is sufficient.
Make callback processing replay-safe
- Use CheckoutRequestID and other provider identifiers to locate the attempt.
- Deduplicate repeated delivery.
- Reject state regressions from stale events.
- Close the invoice atomically so two successes cannot both become the normal satisfying payment.
- Record a second success as an exception requiring investigation.
Protect the endpoint as an internet-facing payment surface
Use HTTPS, accept only the expected HTTP method and content type, validate payload structure, rate-limit abusive traffic, avoid exposing secrets in the callback URL and correlate every event to a request your system actually created.
Do not invent a webhook-signature mechanism that Safaricom has not documented for the product you are using. Security controls should match the actual Daraja contract.
If the callback is missing, query the STK request
For M-PESA Express, use M-PESA Express Query with the CheckoutRequestID when the callback is late or missing.
Do not automatically mark the attempt failed and send another STK Push.
Transaction Status is a different verification path
Safaricom's Transaction Status API operates on transaction identifiers such as an M-PESA receipt number or Originator Conversation ID. It should not replace M-PESA Express Query for an unresolved STK request where CheckoutRequestID is the identifier you have.
Treat callbacks as evidence, not commands
A callback should update the payment-attempt record and then let billing rules decide what follows. It should not directly send a customer message, suspend service or create another charge. That separation makes duplicate delivery and out-of-order delivery easier to handle safely.
Record enough for incident review
Keep the receipt time, CheckoutRequestID, raw ResultCode/ResultDesc, any successful receipt metadata and the internal state transition that was applied. If support later asks why an invoice stayed open or why a retry was blocked, the event history should answer without relying on application logs alone.
Sources
Safaricom M-PESA Express API: https://developer.safaricom.co.ke/apis/MpesaExpressSimulate
Safaricom M-PESA Express Query API: https://developer.safaricom.co.ke/apis/MpesaExpressQuery
Safaricom Transaction Status API: https://developer.safaricom.co.ke/apis/TransactionStatus



