How payment integrations break
The same small set of failures turns up across providers and stacks. They sit between the provider and your database, not in a language or a framework, so WooCommerce and Magento, Odoo and Medusa, and custom backends fail the same way. Here they are in plain terms, with how to tell whether yours has one. The real cases behind each are on the fixed upstream page.
The patterns
-
Acknowledged before it is applied
The webhook answers 200, then the status lookup fails.The provider sends the final status, your endpoint answers 200, and only afterwards tries to fetch or verify the event. When that fetch fails, the 200 has already told the provider everything is fine, so it never resends. The money moved; your database never heard. You see payments that sit pending for hours, with a fetch or verify error in the log just before a 200.
-
Only landed if the buyer comes back
The final status is attached on the return redirect, not by webhook.The payment is tied to the order only when the browser returns to the shop. A buyer who pays and closes the tab, common with 3-D Secure, bank redirects and wallets, is charged while the order is cancelled as unpaid. Look for cancelled or pending orders that line up with no return visit, with redirect methods over-represented.
-
Charged twice on a retry
A lost response is retried without an idempotency key.When the response to a charge is lost to a timeout or a 5xx, the integration marks it failed and tries again, or the same webhook is delivered twice and handled twice. Either way the customer is charged twice, or a renewal is booked twice. Duplicate charges that cluster around timeouts, or one provider event id processed more than once, are the signs.
-
The wrong currency unit
Minor units multiplied or divided by a hundred.Currencies without minor units, such as JPY and KRW, or a mix-up between major and minor units, send the amount to the provider a hundred times too high or too low. The customer is charged a hundred times too much or too little while your side still shows the right number. It shows up as a few currencies that are always wrong, and a gap between your amount and the settled amount.
-
Access without a settled payment
The order completes before the capture clears.The order is marked complete, or a subscription made active, while the capture is still pending, under review or already declined. The customer gets the goods, the level or the plan with no money behind it, and the later denial is never handled. Active subscriptions or completed orders with no matching settled payment are the tell.
-
A cron that races the payment
A scheduled job cancels the order while the payment settles.A background job cancels unpaid orders on a timer, and the real payment settles a moment later through a different path, so a paid order is cancelled. Or a late event stamped with its creation time overwrites a newer status and sends a finished payment back to pending. Watch for paid orders flipped to cancelled by a scheduled task, and statuses that move backwards.
-
A refund or chargeback counted twice, or not at all
The provider notification repeats what your system already did.A refund made in your system is recorded again when the provider's notification arrives, so the order shows twice the refund and the customer gets a second email. Or a dispute is parsed wrong and never recorded at all. Refunded totals that do not match the provider, and disputes missing from the books, point here.
Recognise one of these?
Each is a real case in the list of bugs fixed upstream, found in open-source payment code and reproduced against it before it was reported. The same checks, run read-only on your own logs and database, are a callback delivery audit. A symptom and one payment id are enough to start: hello@terminalstate.dev.