Three charge types, three different refunds
Direct charges, destination charges and separate charges with transfers behave differently on refund. Most leak reports trace back to applying one mental model to all three.
Almost every question we are asked about Connect refunds turns out to be a question about charge types wearing a disguise. Someone read the documentation for one flow, built for another, and now the money is going somewhere they did not expect.
There are three flows. They differ in who holds the funds, who pays the processing fee, and — the part that matters here — what a refund does by default.
The three types
Direct charges
The charge is created on the connected account. Funds land in their balance, they pay the Stripe processing fee, and your platform takes its cut with an application_fee_amount.
await stripe.charges.create(
{ amount: 240000, currency: 'usd', source: token, application_fee_amount: 28800 },
{ stripeAccount: connectedAccountId },
);
The seller is the merchant of record. Their name is on the statement, their account carries the dispute liability, and the money was never yours to begin with.
Destination charges
The charge is created on the platform. Funds land in your balance, you pay the processing fee, and a transfer moves the seller's share to their account as a side effect of the charge succeeding.
await stripe.charges.create({
amount: 240000,
currency: 'usd',
source: token,
application_fee_amount: 28800,
transfer_data: { destination: connectedAccountId },
});
You are the merchant of record. This is the most common flow for marketplaces, and it is where essentially all of the leakage we see lives.
Separate charges and transfers
The charge is created on the platform with no transfer_data at all. Later — possibly much later, possibly split across several sellers — you create transfers explicitly.
await stripe.transfers.create({
amount: 211200,
currency: 'usd',
destination: connectedAccountId,
source_transaction: chargeId,
});
This is the flow for anything where the payout is not a simple one-to-one function of the charge: split baskets, delayed fulfilment, escrow-like arrangements.
What each does on refund
Direct charges
The refund comes out of the connected account's balance, because that is where the money is. Your exposure runs the other way from the destination-charge case — the seller absorbs the refund, and your application fee is the only thing at stake. refund_application_fee decides whether you give your cut back.
The failure mode here is not a leak. It is a negative balance on a seller who has already been paid out, and a support conversation about it.
Destination charges
The refund comes out of your platform balance. The transfer is a separate object and is untouched unless you pass reverse_transfer: true, which defaults to false.
This is the case where a refund can be fully successful and leave the platform materially worse off than before the transaction existed. Every API call returns 200, and the connected account keeps its share of a sale that has been undone.
Separate charges and transfers
The refund and the transfer have no link at all unless you created one. If you passed source_transaction, Stripe can associate them; if you did not, nothing in the data model connects the money that came in to the money that went out, and reconciling them is a join you have to perform yourself using your own records.
The failure mode here is the quietest of the three, because there is no field to check. A missing reversal on a destination charge is visible as amount_reversed: 0 on a transfer that Stripe knows is linked to the charge. A missing reversal here is visible as nothing whatsoever.
The most expensive assumption in a marketplace is that all three of these behave the same way.
Identifying which you are using
From the data
Fetch a recent charge with the transfer expanded and look at two fields.
const charge = await stripe.charges.retrieve(id, { expand: ['transfer'] });
charge.transfer; // set → destination charge
charge.on_behalf_of; // set → settled as the connected account
If charge.transfer is populated, you have a destination charge. If the charge was created with a stripeAccount header — visible as the charge living on the connected account rather than the platform — it is a direct charge. If it is on the platform with no linked transfer, you are on separate charges and transfers, and any transfers exist as independent objects.
From the symptom
A faster diagnostic when you are trying to work out which conversation you are having. If refunds make your platform balance go down and your sellers keep their money, you are on destination charges without reversal. If refunds make your sellers go negative and they are complaining, you are on direct charges. If nothing obviously connects your refunds to your transfers, you are on separate charges and transfers and your reconciliation is your own responsibility.
Common questions
Can one platform use more than one type?
Yes, and this is more common than it sounds — a marketplace that added a subscription product, or a platform that onboarded a category of seller under different terms, frequently ends up with two flows running side by side. It is also how a correct refund path becomes incorrect: the code was right for the flow it was written for, and a new flow arrived that it does not handle. If you use more than one, your refund path needs to branch on the charge, not on an assumption.
Which type should I be using?
That is mostly a question about liability and branding rather than about fees. Destination charges make you the merchant of record, which means disputes land on you and your name is on the statement. Direct charges push both to the seller. Separate charges and transfers give you the most control over timing at the cost of doing your own bookkeeping. There is no answer that is correct for every marketplace, which is why Stripe offers three.
Does on_behalf_of change any of this?
It changes the settlement merchant, which affects the fee structure, the statement descriptor and which account's country rules apply — while leaving the funds flow of a destination charge intact. It is a small parameter with a wide blast radius and it deserves its own treatment.
Destination charge refund leaks
FeeGuard is an independent product and is not affiliated with, endorsed by, or sponsored by Stripe, Inc. "Stripe" and "Stripe Connect" are trademarks of Stripe, Inc., referenced descriptively.