Retry must not double-charge
Aliases: idempotent payment · double charge · payment retry
What it is
After a failure or an apparent non-response, people tap pay again. Retry must not double-charge means the second tap either attaches to the same payment intent or must not open a new one until the first is known uncaptured. It is idempotency of button and intent—not how to query after timeout, and not the SLA for auto-refunding two captures. Those are unknown state and clawback. Here, “tap again” must not become two authorizations.
Why it happens
Pay is not a read. The network is slow, the page still spins, the model is “I missed,” so people tap again. If each tap mints a new merchant order id, the rail treats two payments. Disabling the button only stops same-screen doubles; refresh, back, and a second device still fire. The real constraint is an idempotency key: same order, same amount, same intent id, later requests seen as duplicates. The UI must show “still this payment,” not “a new pay.” When failure is certain, retry is a new intent bound to the same order, stating the previous did not capture before it fires.
Studying it
Under injected delay, log extra taps, pay-after-refresh, and simultaneous pay on two devices. Compare no idempotency, button disable only, intent id throughout.
Independent variables: disable duration, idempotency key used, same intent restored after refresh. Dependent variables: authorizations on the rail, user thinks failed but was charged, tickets for double capture.
Labs tap slowly and miss real doubles. Inject delay. “Only one capture in the end” is not enough—the second auth may have been manually reversed after the scare.
Where it stops holding
An explicit change of amount, coupon, or method is a new intent: open it and void the old; do not reuse the old key and swallow the new amount. Completing a pre-auth is not a retry. Offline POS plus online is two rails; label two sources. After the idempotency window the rail treats a new order; the UI cannot infinitely say “still the earlier one.”
Applying it
- Disable on tap and bind an intent id; swallow further taps until a response, minting no new id.
- On refresh or “incomplete” return from the rail, query that intent before allowing another; when allowed, copy is “continue this order,” not “place again.”
- When failure is certain, show “previous did not capture” before opening a new intent.
- Verify by delaying the pay request several seconds, then extra-tap, refresh, and dual-device pay; reconcile rail authorizations. More than one without a confirmed second payment fails.
Related
- Within the group: H7.06.1 Failure reasons must be understandable and actionable · H7.06.3 Unknown state needs lookup, not a guess
- Adjacent: H1.07 Preventing duplicate submit · H3.10 Retry strategy · H7.13 Payment failure and unknown state
- Search terms:
idempotent payment·double charge·payment retry