Safaricom says 412. Your database says 409.
The reconciliation problem from the point of view of the person who has to do it at month end, and why it is a bookkeeping problem rather than a payments one.
The reconciliation problem from the point of view of the person who has to do it at month end, and why it is a bookkeeping problem rather than a payments one.
It is the twenty-eighth of the month. Somebody is sitting with two windows open.
On the left, the M-Pesa statement, downloaded as a PDF because that is what the portal gives you. On the right, the orders table from the shop's own system, exported to a spreadsheet. One says 412 payments came in on the fourteenth. The other says 409.
Nobody has written down which three, or when, or whether it is three payments or one payment counted wrong three times. The statement goes out at nine tomorrow.
This afternoon is repeated, in some form, in a very large number of Kenyan businesses every month. It is not a technology problem in the sense that nobody has built the technology. It is a bookkeeping problem that most payment integrations are not designed to prevent, and the distinction matters because it tells you where the fix has to go.
There are only a handful of ways, and they are all boring.
The reference was mistyped. A paybill payment arrives with account reference
INV2O41 instead of INV2041. The money is there. The matching is not, because the
matching is a string comparison against a string a person typed on a phone keypad. The
payment is in the statement and is not attached to an order.
The payment was retried. A customer's first attempt timed out, they tried again, and both eventually succeeded. Two payments, one order. Your system shows one, the statement shows two, and refunding the second requires somebody to notice it exists.
The callback was lost. The payment happened. The notification to your server did not arrive, or arrived while your server was restarting. The statement has it, your database does not, and the customer is quite reasonably annoyed that their order says unpaid.
The timing straddles midnight. The payment happened at 23:58 and your system recorded it at 00:01, so it falls in a different day in one record than the other. Both systems are right. The daily totals disagree.
The payment was reversed. Somebody called Safaricom, the transaction was reversed, and nothing told your system, so your books still show revenue that is gone.
Five causes. Each one produces the same symptom, which is a number that does not match, with no information about which rows are involved.
The spreadsheet approach is to sort both lists by amount and time and scan for gaps. This works, in the sense that a determined person will find the three, and it is expensive in three different ways.
It takes hours, and the hours are at month end, which is the one time the person doing it has other things to do.
It is error-prone in a specific and nasty way: a scan that finds three discrepancies stops. If there were four, you have now reconciled to a number that is still wrong and you believe it. The method gives you no way to know the difference between "I found them all" and "I stopped".
And it produces no record. Next month you start from scratch. If the same customer mistypes the same reference every month, nothing accumulates that would tell you.
Not magic. The work is the same work; the difference is that it happens daily, by machine, with a record.
Pull both sides for the same window. Our ledger and the provider's record of the same day. Not the same month: the same day, so that a discrepancy surfaces within twenty-four hours while somebody can still remember what happened.
Match on the strongest thing available. The provider's receipt number, which both sides have and neither side typed. Not the amount, which collides constantly, and not the reference, which is the field that gets mistyped.
Classify what does not match. There are only three shapes, and naming them is most of the value:
Make each one a row with a state. Not a line in a log. A record, with a timestamp, a reason and an open or closed state, so that the question "is there anything unexplained right now" has an answer that is a number rather than an opinion.
That last point is the one that actually changes the month end. The goal is not to never have a discrepancy, because distributed systems guarantee you will. The goal is for every discrepancy to be a known row that somebody has either closed or is looking at, so that at any moment you can say what is outstanding without opening a spreadsheet.
Every system should be able to answer one question: is there money we cannot explain, and how much?
Most cannot, and the reason is not that the engineering is hard. It is that nobody asked for it, because reconciliation lives with the finance team and the payment integration was built by engineers to a specification that ended at "the payment succeeds". The handover between those two worlds is exactly where the three missing payments live.
If you build one thing on top of a payment integration, build the daily comparison and the queue of exceptions. Not the dashboard, not the charts. The boring list of things that do not add up, with a state on each one.
Everything else is reporting. This is accounting, and the difference becomes obvious at nine o'clock on the twenty-ninth.
Ours runs daily and files exceptions into a queue. How it matches, and what it does with what does not, is on the collections page, and the glossary has the two-sentence version.
Genuinely. If something here is wrong, or right for the wrong reason, we would rather be told than keep it published. Corrections get made and credited.