We had a cancel-order flow that called the refund endpoint for everything. Captured payment, refund. Authorized-only payment, also refund. The gateway accepted both calls and returned success, so nobody looked closer.
Then support started getting tickets: "You cancelled my order but the money is still gone from my account." The payments were authorize-only, never captured. The "refund" on an uncaptured auth was being treated by the gateway as a no-op, or in one acquirer's case, as a partial reversal that did not release the hold. The customer's issuer kept the authorization hold for 7 to 10 days, depending on the bank. From the cardholder's side, the money was gone. From ours, the order was cancelled and the ledger showed zero.
The fix was boring. Before cancelling, read the payment state. Authorized and not captured: send a void (reversal). Captured: send a refund. Partially captured: reverse the remainder, refund the rest. We also started logging the issuer response on reversals, because a reversal can be declined too, and then the hold sits there with no error anywhere in our system.
Tickets tied to ghost holds dropped to near zero within a week.
Do you branch on payment state before cancelling, or does your gateway SDK hide the difference for you?
Payneteasy handles this kind of payment flow for merchants and PSPs — see how it works.
Top comments (1)
the declined reversal is the detail nobody plans for: the hold sits there with no error anywhere in your system, which is worse than a failed refund because nothing is monitoring it. logging the issuer response on reversals like it's a first-class transaction state is the fix that generalizes; 'void accepted' and 'void declined' lead to completely different customer conversations a week later. putting the 7 to 10 day hold window in the cancellation email would save support half those tickets too. do you surface the authorization expiry date anywhere, or does the gateway keep that in its admin only?