Skip to content

Failed, Pending, Duplicate, Reversed: Handling the Payments That Break Systems

Any developer can make one payment succeed. Production is about the messy 5%: timeouts, double callbacks, reversals and payments stuck in limbo. That is where money is lost or double-counted.

By Karani Geoffrey, Founder & CEO, Upeosoft
In short

The hard part of payments is not the successful case - it is timeouts, duplicate callbacks, reversals, and payments stuck pending. Handling them reliably means representing a real state machine (pending, paid, failed, reversed), making every operation idempotent so retries never double-charge or double-count, capturing raw callbacks, and reconciling against the provider daily to catch what slips through.

Key takeaways
  • A payment is not simply success or failure - it moves through pending, paid, failed and reversed states.
  • Never treat an initiated payment as complete; only a confirmation callback or a status query proves the outcome.
  • Idempotency - keying every operation so it can safely run twice - is what stops duplicate callbacks double-counting money.
  • Timeouts create the worst state: you don't know if it succeeded, so query status rather than guess.
  • Reversals and refunds must flow back through your ledger, not just be handled on the phone.
  • Daily reconciliation against the provider's records is the safety net that catches whatever your code missed.

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.

Frequently asked questions

Why do duplicate payments happen in software?

Usually because a callback is delivered more than once, or a request is retried after a timeout. Payment providers, including M-Pesa, may resend a confirmation if they are unsure you received it. If your system processes each delivery blindly, it records the same payment twice - inflating revenue or crediting a customer twice. The fix is idempotency: key each transaction so that processing it a second time is recognised and safely ignored.

What should happen when a payment times out?

A timeout is the dangerous case because you genuinely do not know the outcome - the payment may have gone through, or not. The wrong response is to assume failure and let the customer retry, which risks charging them twice. The right response is to leave the transaction in a pending state and actively query the provider's transaction-status API to find out what really happened, then resolve it. Never guess a timeout.

What is idempotency and why does it matter for payments?

Idempotency means an operation produces the same result whether it runs once or many times. For payments it is essential: callbacks get redelivered, users double-click, and retries fire. If each of those can create a new charge or a new credit, you get duplicates and lost money. Building idempotency - via unique transaction keys and checks before acting - means the same payment event can arrive ten times and still be counted exactly once.

How do reversals and refunds fit in?

A reversal or refund is a real financial event that must flow through your system, not just happen on M-Pesa or the card network. If a payment is reversed but your books still show it as received, your accounts are wrong and you may deliver goods against money you no longer have. Your system should record reversals against the original transaction, adjust the ledger and any fulfilment, and keep the audit trail intact.

Isn't reconciliation enough on its own?

Reconciliation is the safety net, not the whole answer. Good state handling and idempotency prevent most errors in real time; reconciliation catches the residue - the callback that never arrived, the timeout that resolved silently, the mismatch. You need both. Relying on reconciliation alone means discovering problems a day later and fixing them by hand; relying on code alone means the rare miss goes unnoticed forever. Together they make payments trustworthy.

Karani Geoffrey
Karani Geoffrey
Founder & CEO, Upeosoft

Karani Geoffrey is the Founder & CEO of Upeosoft, a software and automation company rooted in Kenya. He builds custom software, AI systems, and production-grade ERPNext for businesses across East Africa, and writes about the Kenyan realities - eTIMS, M-Pesa, SHIF, unreliable internet and power - that make or break real systems.

Next step

Want this working in your business?

Upeosoft builds and hardens the systems behind this article - for real Kenyan operations, with eTIMS, M-Pesa and offline realities handled.

Keep reading

Kenyan Compliance and Integrations

How to Integrate the M-Pesa Daraja API Into Your System

The Daraja API is Safaricom's official gateway to M-Pesa. This is the practical path from developer credentials to a live, reconciling integration - and the pitfalls along the way.

6 min readRead article →
Customer Growth and Messaging

How to Automatically Match M-Pesa Payments to Invoices

Matching M-Pesa payments to invoices by hand is slow and error-prone. With a clean account reference and the right setup, your system can do it automatically.

7 min readRead article →
Kenyan Compliance and Integrations

Connecting Payments to Your Accounting System So the Ledger Updates Itself

Accepting a payment is only the first half. If it does not land in your books automatically, you have just moved the manual work from the counter to the accountant's desk. Here is how to close the loop.

7 min readRead article →