This architecture starts where the generic recurring API guide stops
The generic API article describes obligations, attempts and idempotency across providers. This guide maps those concepts specifically onto Safaricom Daraja.
The identifiers you should keep
Merchant invoice ID
Your own stable identifier for the amount due. This should survive every retry and provider call.
Payment attempt ID
Your own identifier for one real attempt to satisfy the invoice.
MerchantRequestID
Returned by M-PESA Express for the request lifecycle. Preserve it with the provider response.
CheckoutRequestID
The critical identifier for the STK request. Store it on the payment attempt and use it to match the callback or query the request if the callback is late.
MpesaReceiptNumber
Returned on successful payment and useful for transaction evidence and later reconciliation.
The recommended state flow
- Create invoice in your billing system.
- Create payment attempt.
- Call M-PESA Express.
- Persist MerchantRequestID and CheckoutRequestID.
- Move attempt to AWAITING_CUSTOMER or PENDING.
- Receive asynchronous callback and match it by CheckoutRequestID.
- Map ResultCode/ResultDesc to an internal final or non-final state.
- On success, store the M-PESA receipt and close the invoice atomically.
- If the callback is missing, use M-PESA Express Query with CheckoutRequestID before retry.
Do not confuse M-PESA Express Query with Transaction Status
M-PESA Express Query checks an STK request using CheckoutRequestID. Transaction Status checks a recent M-PESA transaction when you have an M-PESA receipt number or Originator Conversation ID.
Use the API that matches the identifier and transaction stage you actually have.
Callback handling rules
- Treat the callback endpoint as a machine endpoint, not a browser route.
- Match the event to one known attempt.
- Persist the raw provider result before transforming it.
- Make processing idempotent.
- Do not let a stale non-final event overwrite a final success.
- Do not close the invoice twice.
How manual PayBill C2B fits alongside STK
A customer may pay manually instead of completing the STK request. C2B confirmation can provide TransID, TransAmount and BillRefNumber. Match that transaction to the invoice and mark the STK attempt as superseded or no longer collectible according to your model.
The invoice should close once regardless of which route ultimately paid it. Closing an invoice does not revoke an in-flight STK prompt: stop new initiations, monitor any late result, and handle a second successful collection as a duplicate-payment exception requiring investigation and the applicable refund process.
What to log for support and reconciliation
- Invoice ID and attempt ID.
- MerchantRequestID and CheckoutRequestID.
- Raw ResultCode and ResultDesc.
- M-PESA receipt number on success.
- Amount and transaction timestamp.
- Callback received time and query checks.
These records let support answer what happened without reading application logs manually.
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
- Safaricom C2B API: https://developer.safaricom.co.ke/apis/CustomerToBusiness



