Stop refunding payments you should never have charged
The charge-then-refund flow is everywhere, and it's almost always wrong. Authorize first, run your checks, then capture. Here's the property that does it.
I once shipped a checkout that charged the card the instant the customer hit pay, then ran the order validation afterward. Stock check, address validation, a fraud heuristic, a third-party availability call. When any of those failed, the code did the obvious thing: it refunded the payment.
It worked. It also generated a steady trickle of confused, angry emails. “Why did you charge me 89 euros and then refund it three days later?” To the customer, a charge followed by a refund does not read as “we caught a problem”. It reads as “this company is sketchy and now my money is stuck in limbo for a week”.
The fix was one property I had been ignoring for years: capture_method: manual.
The charge-then-refund antipattern
Here is the flow almost every tutorial teaches, and the one I had shipped:
const intent = await stripe.paymentIntents.create({
amount: 8900,
currency: 'eur',
payment_method: paymentMethodId,
confirm: true,
});
// money has now left the customer's account
const order = await validateOrder(cart);
if (!order.ok) {
await stripe.refunds.create({ payment_intent: intent.id });
throw new Error('Order validation failed after charge');
}
The problem is the comment. By the time you run validateOrder, the money is gone. The charge has hit the customer’s statement, your statement, and in cross-border cases an FX conversion. If validation fails, you are not undoing a mistake, you are issuing a second financial event that has to settle on its own timeline.
That has real costs:
- The customer sees a charge and a refund, days apart, and loses trust
- Refunds can take 5 to 10 business days to land back on the customer’s statement
- You may not get every fee back, and FX swings mean the refund amount can differ from the charge
- A pattern of charges-then-refunds is exactly what card networks flag as a risk signal
You did everything to be honest and it still looks bad. The flow is the problem, not your intentions.
Authorize, then capture
Cards have always supported a two-step model: authorization places a hold on the funds without moving them, and capture actually moves the money. Hotels and car rental companies have used this forever. The hold sits on the customer’s card, the money never leaves until you decide it should, and if you never capture, the hold simply expires.
Stripe exposes this with one property on the PaymentIntent:
const intent = await stripe.paymentIntents.create({
amount: 8900,
currency: 'eur',
payment_method: paymentMethodId,
capture_method: 'manual',
confirm: true,
});
// funds are AUTHORIZED, not captured. Nothing has moved yet.
const order = await validateOrder(cart);
if (order.ok) {
await stripe.paymentIntents.capture(intent.id);
} else {
await stripe.paymentIntents.cancel(intent.id);
}
cancel releases the hold. No charge ever appeared, so there is no refund to explain. The customer sees a pending authorization that quietly drops off, which every cardholder is used to. You moved the risky business logic to where it belongs: between the authorization and the capture, while the money is still reversible without a trace.
What you get for free
Once the payment is authorized but not captured, a few things become possible that the charge-first flow makes painful.
Partial capture. You can capture less than you authorized. Authorize 89 euros for a cart, discover one item is out of stock, capture 64 euros and the rest of the hold releases automatically:
await stripe.paymentIntents.capture(intent.id, {
amount_to_capture: 6400,
});
A real validation window. Stripe holds an uncaptured card authorization for about 7 days before it expires on its own. That is plenty of time for a synchronous stock check, and enough for some asynchronous flows too. If you never capture, you never charged.
A clean audit trail. “Authorized, then cancelled” is a single coherent story in your logs and in the customer’s mind. “Charged, then refunded” is two events that have to be reconciled, and that reconciliation is where money quietly goes missing.
This is not Stripe-specific
The property name changes, but every serious payment provider exposes the same authorize-then-capture model. If you are building anything provider-agnostic, this is the pattern to standardize on:
- Stripe:
capture_method: 'manual'on the PaymentIntent, thenpaymentIntents.capture()orpaymentIntents.cancel() - Adyen: set a manual capture delay on the account or request, then call
/paymentsto authorize and/payments/{id}/capturesto capture - Braintree: pass
submitForSettlement: falsetotransaction.sale, thentransaction.submitForSettlement(id)later - PayPal: create the order with
intent: 'AUTHORIZE', then capture the authorization
The vocabulary differs, the shape is identical: get a hold, do your work, settle or release.
When you should not use it
Manual capture is not free of trade-offs, and it is the wrong tool in a few cases.
- Instant digital goods. If you deliver the moment payment succeeds and there is nothing to validate, the extra round trip buys you nothing. Capture immediately.
- Subscriptions and recurring billing. These run on their own automated capture flow. Do not bolt manual capture onto a subscription unless you have a specific reason.
- Validation slower than the hold. If your checks can take longer than the authorization window, the hold may expire before you capture. Either speed up the checks or rethink the flow.
- Methods that do not support it. Some non-card payment methods do not offer separate auth and capture. Confirm support for the methods you actually accept before you build around it.
The rule I follow now
If there is any business logic that can fail after the customer has paid but before you are willing to keep their money, that logic belongs between an authorization and a capture. Not before a charge, and definitely not after one.
The charge-then-refund flow is the default in almost every example online, which is exactly why it ends up in production. One property moves the risky work to the right side of the money, and the angry emails stop.
If Stripe’s own fees are also a concern for your volume, see Stripe alternatives in the EU for providers that support the same authorize-then-capture model.