All docs

Destination charge refunds — who absorbs the loss

On a destination charge, refunding without reverse_transfer means the platform absorbs the full refund while the connected account keeps the transfer.

How destination charges settle

A destination charge is created on the platform account with transfer_data[destination] set. Stripe collects the funds into the platform balance, then immediately transfers the amount minus the application fee to the connected account.

The platform is the merchant of record. It carries the liability for refunds and disputes.

What a refund does by default

A refund debits the platform balance for the full refunded amount. The transfer already made to the connected account is untouched.

The platform has now paid the buyer back and paid the seller. On a $100 charge with a $10 fee, a full refund without reverse_transfer costs the platform $90 of its own money.

The three positions to reconcile

Any reconciliation of a destination charge needs all three numbers: what was refunded to the buyer, what was reversed from the connected account, and what happened to the application fee.

Checking one in isolation produces a confidently wrong answer, which is worse than not checking at all.

Destination charges versus the alternatives

Stripe Connect offers three charge types, and the refund liability differs in each. Choosing one is choosing who absorbs refunds by default.

Direct charges are created on the connected account. They are the merchant of record, they carry refund and dispute liability, and your platform takes an application fee. Simplest liability model, least platform control.

Destination charges are created on your platform account with transfer_data[destination]. You are the merchant of record and carry the liability. Most control, most reconciliation burden — and the topology this entire article is about.

Separate charges and transfers are created on your platform with transfers issued independently. Same liability as destination charges, plus you own the charge-to-transfer mapping yourself.

A worked example

A $100 order on a platform taking a 10% application fee, fully refunded without reverse_transfer:

Stripe collected $100 into your balance and transferred $90 to the seller, leaving you $10. The refund debits your balance $100. The seller keeps $90.

Your net position is −$90 on an order you earned $10 from. You have paid the buyer back out of your own funds and paid the seller in full for a sale that no longer exists.

At a 6% refund rate on $5M annual volume, that is $300,000 of refunded volume. Even recovering only the portion where the flag was missed on partial refunds typically runs to five figures a year.

The three positions you must reconcile

Any correct reconciliation of a destination charge needs all three numbers together:

1. What was refunded to the buyer — charge.amount_refunded.

2. What was reversed from the connected account — transfer.amount_reversed.

3. What happened to the application fee — application_fee.amount_refunded.

Checking any one in isolation produces a confidently wrong answer, which is worse than not checking at all, because it produces a number people act on. The measure that holds is the net: (collected fee − refunded fee) − (original transfer − reversed transfer). Negative means you are carrying the loss.

const charge = await stripe.charges.retrieve('ch_123', {
  expand: ['transfer', 'application_fee'],
});

const transfer = charge.transfer as Stripe.Transfer;
const fee = charge.application_fee as Stripe.ApplicationFee;

const netPlatformMargin =
  (fee.amount - fee.amount_refunded) -
  (transfer.amount - transfer.amount_reversed);

On_behalf_of does not change liability

A destination charge can carry on_behalf_of, which sets the connected account as the settlement merchant. It changes what appears on the buyer's statement and which account's pricing and fee structure applies.

It does not move refund liability. The charge still lives on your platform, refunds still debit your balance, and disputes are still yours. Teams occasionally set it believing they have transferred risk, and only discover otherwise during their first significant refund month.

If the goal is genuinely to move liability, the mechanism is direct charges — with everything that implies about support, statements, and network monitoring.

Switching topology is not a small migration

Platforms that discover this liability sometimes conclude they should move to direct charges and push refund liability onto their sellers. That is a legitimate destination, and it is a larger change than it looks.

Direct charges change who the merchant of record is, which changes what appears on the buyer's card statement, who handles buyer support, who is exposed to card network monitoring for excessive disputes, and how your application fee is collected. In many markets it also changes who bears regulatory obligations.

It also removes control. On destination charges you can reverse a transfer when a seller disappears. On direct charges you never held the funds.

Most platforms are better served by keeping destination charges and fixing the reconciliation, which is a bounded engineering problem, than by changing their entire payments topology to avoid it.

What to instrument if you build this yourself

Four things, in the order they usually get discovered the hard way.

Re-read live state. Reconcile against a fresh retrieve, not the webhook payload, which is a snapshot from when the event was queued.

Serialise per charge. Two events about the same charge processed concurrently will both read pre-reversal state and both raise the same finding.

Delay refund-event processing by a few seconds. charge.refunded and transfer.reversed arrive within seconds of each other in either order, and Stripe guarantees no ordering between them.

Paginate every list call. A long-lived subscription charge can accumulate more refunds and reversals than a single 100-item page holds. A truncated sum understates what has already been recovered, producing a false positive on exactly the charges with the most activity.

Why this is invisible until close

Every API call in the leaking path succeeds. The refund returns 200. The webhook fires. No error is logged, no alert fires, and no dashboard turns red.

The only symptom is that your platform balance is lower than your ledger predicts — which surfaces at month-end, in aggregate, weeks after the individual transactions that caused it and long after the recovery window has closed.

That delay is the whole problem. The reconciliation is not hard; noticing in time to act on it is.