Reconcile M-Pesa transactions by comparing your database records against M-Pesa statements daily. Use callbacks as your primary data source, polling as a fallback for missed callbacks, and a scheduled reconciliation job to catch anything that slipped through. Store the M-Pesa receipt number as your single source of truth for matching records.
If you have built an M-Pesa integration and it works in sandbox, congratulations. You are maybe 60% done. The other 40% is making sure your database and M-Pesa agree on what happened, every single time, even when the network drops, your server restarts, or Safaricom's systems hiccup.
Here is what goes wrong in production:
Every M-Pesa integration that handles real money needs three layers of defense: real-time callback processing, a polling fallback for missed callbacks, and a scheduled reconciliation job that catches everything else. Skipping any one of these layers means you will eventually lose money, double-charge a customer, or spend hours manually tracing transactions.
The rest of this guide walks through each layer and shows you how to build them.
Your database schema determines how easy or painful reconciliation will be. Get this right upfront and everything else becomes simpler. Get it wrong and you will be running raw SQL queries at midnight trying to figure out why your numbers do not add up.
Essential columns for your transactions table:
id: Your internal primary key (UUID or auto-increment)merchant_request_id: Returned by Daraja when you initiate the STK Push. This is your link between the request and the callbackcheckout_request_id: Also returned on initiation. Some callback payloads reference this instead of merchant_request_idmpesa_receipt_number: The M-Pesa transaction code (e.g., "QKJ4ABCDEF"). This is the single most important field for reconciliation. It is what appears on the user's M-Pesa statement and what Safaricom uses internallyphone_number: The payer's phone number in 254 formatamount: The transaction amount as a decimal, not an integer. M-Pesa can process fractional amountsstatus: An enum with values like initiated, pending, completed, failed, reversed, reconciledresult_code: The numeric result code from Daraja's callbackresult_description: The human-readable result description from the callbackcallback_raw: The full raw JSON payload from the callback. Store this as a JSON column or text. You will need it during disputesinitiated_at: When your server sent the STK Push requestcompleted_at: When you received and processed the callbackreconciled_at: When the daily reconciliation job confirmed this transaction matches the M-Pesa statementWhy status enums beat boolean flags. A common beginner pattern is to have a paid boolean column. This breaks immediately. What does paid = false mean? That the payment failed? That it is still processing? That it was reversed? You cannot tell. An explicit status enum with defined transitions (initiated can become pending, pending can become completed or failed, completed can become reversed) gives you a state machine that catches invalid transitions. If something tries to set a "completed" transaction back to "pending," you know something is wrong.
Add indexes on the columns you query during reconciliation: mpesa_receipt_number, merchant_request_id, status, and initiated_at. Your daily reconciliation job will query transactions by status and date range. Without indexes, these queries get slow as your table grows.
Keep a separate transaction_events or transaction_logs table that records every status change with a timestamp. When a transaction moves from "pending" to "completed," log that event. When it moves from "completed" to "reversed," log that too. This audit trail is invaluable when you need to reconstruct what happened to a specific transaction during a customer dispute.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Daraja's callback mechanism is your first and fastest source of transaction data. When a user completes (or fails) an M-Pesa payment, Daraja sends a POST request to the callback URL you specified when initiating the STK Push. In the happy path, this arrives within seconds.
Making your callback handler production-ready:
First, respond fast. Your callback endpoint should acknowledge the request (return HTTP 200) as quickly as possible. Do not perform heavy processing before responding. Parse the payload, validate the basic structure, enqueue it for processing, and return 200. Process the business logic asynchronously. If your callback handler takes too long, Daraja may time out and retry, creating duplicate processing risk.
Second, implement idempotency. Check whether you have already processed a callback for this MerchantRequestID or CheckoutRequestID before doing anything. If you have, return 200 and skip all processing. The simplest implementation: add a unique constraint on merchant_request_id in your transactions table. If the insert fails due to a duplicate key violation, you know it is a retry.
Third, validate the payload. Daraja callbacks follow a specific JSON structure. If you receive a POST to your callback URL that does not match this structure, reject it. This protects against malicious actors who discover your callback URL and try to forge payment confirmations. In production, also validate the source IP against Safaricom's known IP ranges.
What callbacks do not cover:
Callbacks handle the majority of your transactions. For the rest, you need a fallback.
Polling means actively asking Daraja "what happened with this transaction?" instead of waiting for the callback. You do this using the Transaction Status Query API or, for STK Push specifically, the STK Push Query API.
When to poll: Run a background job that queries your database for transactions stuck in "initiated" or "pending" status for longer than a reasonable threshold. For STK Push, if you have not received a callback within 3 to 5 minutes, something probably went wrong with the callback delivery. Query Daraja to find out the actual status.
STK Push Query API: This endpoint lets you check the status of a specific STK Push request using the CheckoutRequestID from the original request. The response tells you whether the transaction completed, failed, or is still pending. If it completed, the response includes the M-Pesa receipt number, which you can use to update your database.
Transaction Status Query API: A more general endpoint that checks the status of any M-Pesa transaction using the transaction ID. Useful for B2C payouts and other transaction types beyond STK Push.
Implementation pattern:
SELECT * FROM transactions WHERE status IN ('initiated', 'pending') AND initiated_at < NOW() - INTERVAL '5 minutes'Rate limit awareness. The query APIs have their own rate limits. If you have 500 pending transactions, do not fire 500 API calls simultaneously. Process them in batches with a short delay between each call. A queue with a configurable concurrency limit (say, 5 concurrent requests) works well.
Do not over-poll. Polling every 10 seconds for a transaction that was just initiated is wasteful and risks hitting rate limits. Give the callback a reasonable window (3 to 5 minutes) before falling back to polling. Most callbacks arrive within seconds. Polling is for the exceptions, not the normal flow.
Between callbacks and polling, you should have visibility into the vast majority of your transactions. The daily reconciliation job handles the remaining edge cases.
Even with callbacks and polling, mismatches accumulate. A transaction might complete on M-Pesa's side but your database shows "failed" because of a parsing error in your callback handler. Or a reversal might happen hours after the original payment. Daily reconciliation catches these discrepancies before they become bigger problems.
Getting M-Pesa statements. Safaricom provides transaction statements through the Daraja portal and, for some account types, through an API. Download your daily statement (usually available in CSV or Excel format). This statement lists every transaction that hit your shortcode in the past 24 hours: incoming payments, outgoing payouts, reversals, and fees.
The reconciliation process:
mpesa_receipt_numberHandling reversals. When a user reverses an M-Pesa transaction through Safaricom, the reversal appears on your statement as a separate line item. Your reconciliation job should detect reversals by matching the reversed transaction's receipt number. When found, update the original transaction status to "reversed" and trigger whatever business logic is needed (cancel the order, revoke access, etc.). If you do not actively check for reversals, you will discover them only when your monthly accounts do not balance.
Automating the job. Run your reconciliation as a scheduled task (cron job, scheduled cloud function, or a task queue). Parse the statement, compare records, generate a report, and email the summary to your finance team. The report should list: total transactions reconciled, number of mismatches found, total amount discrepancy, and individual exception details. Keep the job idempotent so running it twice for the same day does not create duplicate exception records.
When to escalate. Small mismatches (a few transactions per day) are normal and usually caused by timing differences between your server clock and M-Pesa's. Systemic mismatches (dozens of unmatched transactions, or a growing trend of discrepancies) indicate a problem in your callback handler, polling logic, or network configuration. Investigate the root cause rather than just resolving individual exceptions.
These are not hypothetical scenarios. They happen in production. Build handling for each one before launch, not after your first customer complaint.
Partial payments. The user's M-Pesa balance is lower than the order amount. Some businesses allow partial payments and expect the user to "top up" with a second transaction. If you support this, your database needs to track partial payments against an order, calculate the remaining balance, and only mark the order as fully paid when the sum of all payments meets or exceeds the order total. Your reconciliation job needs to match multiple M-Pesa transactions to a single order. This adds complexity, so decide early whether partial payments are worth supporting for your use case.
Reversed transactions. As mentioned above, users can call Safaricom and reverse transactions. The reversal can happen hours or even days after the original payment. Your system should check for reversals during daily reconciliation and have a process for handling the downstream effects (revoking delivered services, flagging the user's account, notifying your support team). Some businesses also implement a hold period before delivering high-value goods or services, giving the reversal window time to pass.
Amount mismatches. The user was supposed to pay KES 500, but the M-Pesa callback shows KES 450. This can happen if the user manually enters an amount (for paybill payments as opposed to STK Push, where the amount is fixed). Your callback handler should compare the received amount against the expected amount. Accept the payment but flag it as underpaid. Whether you fulfill the order, refund the partial payment, or ask the user to send the difference depends on your business rules.
Duplicate phone numbers. Two different users share a phone number (common in families) or a user changes their phone number. Your system should tie transactions to user accounts, not just phone numbers. Use the internal order ID or reference number as the primary link, with the phone number as a secondary identifier.
Timezone issues. M-Pesa timestamps use East Africa Time (EAT, UTC+3). If your server runs in UTC (which is common for cloud servers), a transaction at 11:30 PM EAT on Monday appears as 8:30 PM UTC on Monday. During reconciliation, a transaction might appear on different "days" depending on which timezone you use. Pick one timezone for all your date-based queries and stick with it. Document the choice so future developers do not introduce bugs by assuming a different timezone.
M-Pesa maintenance windows. Safaricom schedules maintenance on M-Pesa systems, usually late at night. During maintenance, callbacks may be delayed or lost. Your polling fallback should be especially vigilant during and after maintenance windows. Check Safaricom's developer communication channels for maintenance announcements and adjust your monitoring thresholds accordingly.
You do not need to build everything from scratch. Here are practical tools and approaches that reduce the reconciliation workload.
Database tools. PostgreSQL is a solid choice for transaction storage. Its JSON column type handles raw callback payloads cleanly. Its advisory locks prevent concurrent callback processing races. And its robust indexing handles reconciliation queries at scale. If you are on MySQL, the JSON column type works similarly. SQLite is fine for prototyping but avoid it in production for financial data because it does not handle concurrent writes well.
Queue systems. For the polling fallback and reconciliation jobs, a simple task queue prevents you from overwhelming Daraja's APIs. BullMQ (Node.js), Celery (Python), or even a basic database-backed queue works. The key feature you need is retry with backoff: if a query fails, retry it after a delay rather than immediately.
Monitoring. Set up alerts for: callback endpoint downtime (uptime monitoring on your callback URL), transaction failure rate spikes, reconciliation exceptions above your threshold, and pending transactions older than your timeout window. Tools like UptimeRobot (free tier available) can monitor your callback URL. Application-level monitoring through Sentry, LogRocket, or even simple email alerts works for the rest.
Testing reconciliation. Create a test script that generates mock M-Pesa statement data and runs your reconciliation job against it. Include test cases for all the edge cases: missing transactions, reversed transactions, amount mismatches, and duplicate receipts. Run this test suite before deploying any changes to your reconciliation logic.
If you want hands-on practice building a complete M-Pesa integration with proper reconciliation, callback handling, and production deployment, our M-Pesa Integration for Developers course (KES 9,999) covers exactly this. You build a working payment system with a real Daraja sandbox, not just the happy path but the failure modes that matter in production.
KES 9,999
Master Daraja API, STK Push, C2B, B2C, and callback handling. Build a real payment flow from scratch. 10 modules, 57 lessons.
Enrol in M-Pesa Integration