The demo works. Production is a different sport.
Every payment integration passes its first test: send a request, money moves, everyone celebrates. Then it meets reality, where a small but constant fraction of payments do not behave. The prompt times out. The callback arrives twice. A customer is refunded. A network blips at the worst moment.
That messy minority is the entire game. It is where money gets lost, double-counted, or delivered against payments that never cleared. A payment integration is judged not by the happy path but by how gracefully it handles the ugly one - and that is what separates a weekend demo from revenue infrastructure.
A payment is a state machine, not a yes/no
The root mistake is modelling a payment as a boolean: paid or not. Real payments move through states, and each transition matters.
A payment starts pending - initiated but unconfirmed. It becomes paid only on a genuine confirmation. It can become failed - cancelled, declined, timed out. It can later become reversed or refunded. If your data model cannot represent 'pending', your code is forced to guess between success and failure at exactly the moment it should not. Modelling the full lifecycle explicitly is the foundation everything else builds on.
Never trust an initiation - confirm it
The single most common production bug is treating a sent request as a completed payment. You fire an STK Push, get an acknowledgement that it was accepted, and mark the order paid. But acceptance is not payment - the customer may cancel, let it time out, or have insufficient funds.
Only a confirmation callback with a success result, or a transaction-status query, proves money actually moved. Until then the payment is pending, and your system should behave accordingly: do not release goods, do not mark the invoice settled, do not tell the customer they have paid. This one discipline prevents a whole category of 'we shipped but never got paid' losses.
Idempotency: the antidote to duplicates
Duplicates are inevitable unless you design against them. Providers redeliver callbacks when unsure you received them. Users double-click. Retries fire after timeouts. Without protection, each of these creates a second charge or a second credit.
Idempotency is the answer: give every transaction a unique key, and before acting on any payment event, check whether you have already processed that key. If you have, acknowledge it and do nothing more. Now the same confirmation can arrive ten times and still be counted once. The same principle protects outbound payments - a retried payout keyed idempotently can never pay a supplier twice. This is not advanced engineering; it is the baseline that makes payments safe.
Timeouts and the unknown outcome
Timeouts are the worst state because they are genuinely ambiguous: the payment may have succeeded, may have failed, and you cannot tell from the silence. Handling them badly causes both classic disasters - assume success and you deliver for free; assume failure and let the customer retry, and you charge them twice.
The correct move is to resolve the ambiguity, not paper over it. Leave the transaction pending, then use the provider's transaction-status query to ask what actually happened, and only then transition it to paid or failed. A payment system that actively reconciles its own timeouts, rather than guessing, is one you can trust with money.
Reversals, refunds and the ledger
Money sometimes flows backwards: a mistaken payment is reversed, a customer is refunded, a chargeback claws funds back. These are real financial events, and they must live in your system, not just on the phone or in the card network.
A reversal should attach to the original transaction, move the payment out of 'received', and adjust anything downstream - the invoice, the stock, the commission that was calculated on it. If your books show money you no longer hold, every report built on them is wrong. Handling the backward flow with the same care as the forward flow keeps your accounts honest.
Reconciliation: the safety net that catches the rest
Even with a clean state machine and solid idempotency, some events will slip - a callback that never arrived, a status that resolved outside your view. Daily reconciliation is the net that catches them.
Every day, compare what your system recorded against the provider's own statement - M-Pesa, the bank, the gateway. Investigate every mismatch: a payment they show and you missed, or one you show and they do not. This closes the loop, surfaces silent failures within a day instead of never, and gives you confidence that your books match reality. Code prevents most errors; reconciliation guarantees you find the ones that got through.
How Upeosoft builds payments that don't break
We build payment handling for the hard cases first: an explicit state machine, confirmation-only settlement, idempotent processing so duplicates and retries never double-count, active status queries to resolve timeouts, proper reversal handling, and automated daily reconciliation against every provider.
If your current integration occasionally double-charges, loses payments, or shows numbers that never quite match M-Pesa, that is exactly the class of problem we fix. Talk to Upeosoft and we will make your payments reliable at the edges, not just the demo.
