All docs

Documentation

reverse_transfer — the flag that decides who eats the refund

reverse_transfer decides whether your platform or your connected account absorbs a refund. What it does, what it does not, and how partial refunds behave.

What the flag does

reverse_transfer: true on a refund tells Stripe to create a proportional reversal on the transfer associated with the charge. Funds move back from the connected account to the platform.

It is opt-in. The default is false, which means the platform absorbs the refund by default — a defensible API design, and a costly one if nobody on your team knew.

Partial refunds

For a partial refund, Stripe reverses the same proportion of the transfer as the proportion of the charge refunded. A 30% refund on a $100 charge with a $90 transfer creates a $27 reversal.

Rounding is applied per reversal, so a charge refunded across several partial refunds can end up a cent or two away from a single calculation over the total. Any reconciliation check needs a small tolerance, or it will report noise forever.

What it does not do

It does not refund your application fee. That is a separate flag — refund_application_fee — and the interaction between the two is where platforms most often end up double-counting.

It does not guarantee the money arrives. If the connected account has insufficient balance, the reversal still succeeds but leaves them with a negative balance that Stripe recovers from their next inbound volume.

Reversing after the fact

If the refund already happened without the flag, you can still create a standalone reversal. This is the recovery path, and the amount is capped at the transfer balance still outstanding.

await stripe.transfers.createReversal('tr_123', {
  amount: 2700,
  refund_application_fee: true,
});

When the connected account has no balance

A reversal against an account with insufficient balance still succeeds. Stripe creates it and leaves the connected account with a negative balance, which it recovers from their next inbound volume.

That is fine for an active seller and worthless for a dormant one. If they never transact again, the platform absorbs the loss regardless of having done everything correctly.

This is the single strongest argument for reconciling continuously rather than at month-end. A reversal created within the payout window draws against funds Stripe is still holding. The same reversal three weeks later draws against a balance that has already left.

Separate charges and transfers

Everything above assumes destination charges, where the transfer is linked to the charge and reverse_transfer can find it.

On separate charges and transfers, the transfer is created independently and the charge carries no transfer field. reverse_transfer has nothing to act on, and a refund will never reverse anything no matter what flags you pass.

Reconciling that topology means tracking the charge-to-transfer relationship yourself — usually via metadata on the transfer — and creating reversals explicitly. The arithmetic is identical; only the lookup changes.

// Separate charges and transfers: link them yourself
const transfer = await stripe.transfers.create({
  amount: 9000,
  currency: 'usd',
  destination: 'acct_123',
  transfer_group: 'order_456',
  metadata: { charge_id: 'ch_123' },
});

// …then find it again at refund time
const transfers = await stripe.transfers.list({ transfer_group: 'order_456' });

Always pass an idempotency key

A transfer reversal cannot itself be reversed. Undoing a double-reversal means creating a fresh transfer back to the connected account — a manual process, and an awkward conversation with a seller who has just been debited twice.

Any retry path, whether a job queue or a human clicking twice, needs an idempotency key derived from something stable: the charge id, the dispute id, or your own internal record id.

Note that Stripe idempotency keys expire after 24 hours. A retry the next day genuinely re-attempts, so a job that has been stuck in a dead-letter queue overnight needs its state checked before being replayed rather than blindly retried.

await stripe.transfers.createReversal(
  'tr_123',
  { amount: 2700, refund_application_fee: true },
  { idempotencyKey: `reversal-${chargeId}` },
);

Reversing a refund that has already happened

A refund issued without the flag is not unrecoverable. The transfer is still there, and a standalone reversal can be created against it at any point while the connected account has balance.

The amount is capped at what remains: transfer.amount − transfer.amount_reversed. Requesting more is rejected, which is why any recovery job should recompute the cap at execution time rather than trusting a figure calculated when the problem was first detected. Between detection and action, someone may have reversed part of it manually.

Set refund_application_fee: true on the reversal when the charge carried an application fee and you intend to return it, so the net position lands where your reconciliation said it should rather than requiring a second corrective action.

What reversal does to the seller

From the connected account's side, a reversal is a debit they did not initiate. Handled badly it generates a support ticket and erodes trust; handled well it is unremarkable administration.

Two things make the difference. First, terms: state plainly, before you ever need it, that refunded and disputed transactions may be reversed from connected account balances. Recovering funds under an agreed term is routine. Recovering them under no term is a negotiation you will usually lose.

Second, notice: a message naming the order, the reason, and the amount converts an alarming debit into an expected one. Most sellers accept it readily — the money was never really theirs once the buyer was refunded.

Reversals that push an account negative deserve particular care. Stripe will recover the balance from their future volume, which means the seller experiences it as reduced payouts over the following weeks rather than a single debit. Explaining that up front prevents a second round of confusion.

Verifying it worked

transfer.amount_reversed on the parent object is the authoritative total, but it can briefly lag a reversal created moments earlier.

Summing the reversal list as a cross-check and taking the larger of the two biases toward under-reporting a shortfall — the safe direction, since a missed alert costs less than a reversal issued against money that already came back.