Stripe Connect Platform Fees: Destination Charges vs Separate Charges and Transfers
How Stripe Connect platform fees really work — destination charges vs separate charges and transfers, application_fee_amount, and who pays for chargebacks.
Use destination charges when one connected account gets paid and you know who it is at checkout. Use separate charges and transfers when you need to hold the money, split it between multiple accounts, or decide the split after the payment. That's the decision — everything else is detail, but the details determine who pays for a chargeback six months from now.
Both models make your platform the merchant of record, and in both, Stripe debits *your* account for the processing fees, refunds, and chargebacks. That surprises founders who assumed the seller's balance absorbs it. Below is how the money actually moves in each, what it costs, and the three constraints that usually make the choice for you.
The three Connect charge types
Stripe Connect offers three ways to route a payment, and they differ mainly in whose account the charge lands on and who carries the risk.
- Direct charges — the charge is created on the connected account. They're the merchant of record; Stripe attempts to debit disputes from their balance first. Best for platforms where sellers are clearly the ones selling, like a hosted-storefront product.
- Destination charges — the charge lands on your platform, then funds transfer immediately and automatically to one connected account. You're the merchant of record. This is the right default for most marketplaces.
- Separate charges and transfers — the charge lands on your platform and the transfers are decoupled from it, fired whenever you choose, to as many accounts as you need. You're the merchant of record and you carry the most risk.
Destination charges: the sensible default
A destination charge is a single API call that charges the customer and pays the seller at the same time. You set transfer_data[destination] to the connected account and application_fee_amount to your cut, and Stripe handles the rest.
// Destination charge: money moves to the connected account immediately.
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price_data: { currency: "usd", unit_amount: 10000,
product_data: { name: "Venue deposit" } }, quantity: 1 }],
payment_intent_data: {
// Your cut, in the smallest currency unit. 10% of $100.00 here.
application_fee_amount: 1000,
transfer_data: { destination: connectedAccountId },
// Required when the platform and the account are in different regions.
// Makes the connected account the settlement merchant.
on_behalf_of: connectedAccountId,
},
success_url: `${SITE}/success?session_id={CHECKOUT_SESSION_ID}`,
});The money flow is worth understanding precisely, because it explains where the Stripe fee lands. The full charge amount transfers from your platform to the connected account once the charge is captured. Your application_fee_amount is then transferred back to you, and Stripe's processing fee is deducted from *your* side of that split — not the seller's. So on a $100 charge with a $10 application fee, the seller nets $90 and you net $10 minus Stripe's fee on the full $100. Price your take rate with that in mind; platforms that set a 3% fee and then discover Stripe's fee comes out of their 3% have a very short runway.
One parameter deserves special attention. If your platform and the connected account are in different regions, you must pass on_behalf_of to make the connected account the settlement merchant. That also means the connected account needs both the recipient and merchant configurations enabled during onboarding — miss it and payments fail at go-live, which is a bad time to find out.
Separate charges and transfers: for holds and multi-party splits
Separate charges and transfers decouple the payment from the payout. You charge the customer with a transfer_group, then create transfers against that group whenever the business logic says to — on delivery confirmation, at the end of a return window, after manual approval, or split across several accounts at once.
// Separate charges and transfers: charge now, decide the split later.
const intent = await stripe.paymentIntents.create({
amount: 10000,
currency: "usd",
transfer_group: `ORDER_${orderId}`, // ties the charge to later transfers
});
// ...hours or days later: delivery confirmed, return window closed.
await stripe.transfers.create({
amount: 7000,
currency: "usd",
destination: restaurantAccountId,
transfer_group: `ORDER_${orderId}`,
// Without this, the transfer FAILS if your available balance is short.
// With it, Stripe waits until the charge's funds have settled.
source_transaction: chargeId,
});
await stripe.transfers.create({
amount: 2000,
currency: "usd",
destination: courierAccountId,
transfer_group: `ORDER_${orderId}`,
source_transaction: chargeId,
});
// $100 charged, $70 + $20 transferred out, $10 stays with the platform.The parameter that separates a working integration from a 2am incident is source_transaction. By default a transfer fails if it exceeds your platform's available balance, and Stripe does not automatically retry failed transfers. Setting source_transaction to the originating charge makes the request succeed immediately and defers execution until those specific funds have settled. Without it, your automatic payout schedule can sweep the balance before your transfers run, and sellers don't get paid.
Stripe's own guidance is blunt: use separate charges and transfers only when you're prepared to be responsible for your connected accounts' negative balances. That's the trade for the flexibility.
Escrow-style holds are a business decision with a compliance shadow. Holding other people's money for weeks is a different regulatory conversation than passing it straight through.
Who actually pays for a chargeback
For destination charges and separate charges and transfers — with or without on_behalf_of — Stripe debits the dispute amount and the dispute fee from your platform account. The connected account is not touched automatically. This is the single most expensive thing to learn late.
Recovering the money is your job, and it's a deliberate step: listen for charge.dispute.created and reverse the transfer to claw funds back from the connected account. If they've already been paid out and their balance is negative, Stripe will attempt to debit their external account only if debit_negative_balances is enabled. Otherwise you're chasing a seller for money.
There's a nasty edge case on cross-border destination charges with on_behalf_of: if you reverse the transfer and then *win* the dispute, cross-border transfer restrictions may leave you unable to send the funds back. Stripe's recommendation is to wait until a cross-border dispute is actually lost before reversing. Direct charges behave differently again — Stripe tries the connected account's balance first, and who pays the dispute *fee* depends on your controller.fees.payer configuration.
All of this runs through webhooks, which is why dispute handling belongs in the same idempotent, retry-safe pipeline as everything else. If you haven't hardened that layer, start with Stripe webhooks in production before you add Connect on top of it.
Geography quietly makes the decision for you
Before you weigh anything else, check whether your funds flow is even supported. Separate charges and transfers are available in around 40 countries, and cross-border transfers on the payments balance work between the US, Canada, the UK, the EEA, and Switzerland. Outside those corridors, your platform and connected accounts generally have to be in the same region.
I've watched this reshape more than one architecture late in a build. When I worked on payments for the Dryva courier platform, the payout model had to fit the region it operated in, not the model that looked neatest on a whiteboard. Confirm supported corridors for your actual markets on day one — it's a ten-minute check that can save a rebuild.
How to choose, in order
Work through these in sequence and stop at the first one that gives you a clear answer. Most platforms have their decision made by question three.
- Is the funds flow supported in your markets? Check the country list and cross-border corridors before designing anything.
- Who should be the merchant of record? If sellers must appear as the merchant and carry their own disputes, use direct charges.
- Do you know the recipient at checkout, and is there exactly one? Yes to both, and you want destination charges.
- Do you need to hold funds or split across multiple accounts? That's separate charges and transfers — and you're accepting negative-balance responsibility.
- Can your balance absorb a bad week of chargebacks? If not, prefer the model that moves less money before you've confirmed delivery.
Common questions
Can I switch charge types later?
Technically yes, and it's a bigger project than it sounds. Charge type affects onboarding requirements and account capabilities, your reconciliation and reporting, refund and dispute handling, and your tax position as merchant of record. Historical payments stay on the old model, so you run both paths for a while. Choose deliberately at the start.
How do refunds work when I've already transferred the funds?
Stripe debits your platform for the refund, and you reverse the associated transfer to recover it from the connected account. Transfer reversals can be partial, and you can choose whether to refund the related application fee — most platforms return their fee on a full refund and keep it on partial ones. Decide that policy before launch and write it into your seller terms.
What does Stripe charge for Connect itself?
Connect is priced on top of standard processing: broadly, a monthly fee per actively paid-out account plus a percentage on payout volume, varying by country and account type. Because it's per active account, a platform with many low-volume sellers has a very different cost profile from one with a few high-volume ones. Model it against your actual seller distribution rather than a headline rate, and check Stripe's current pricing page for your country.
Getting this right the first time
The expensive mistakes in Connect are architectural, not syntactic: the wrong charge type, no plan for disputes, and transfers without source_transaction. None of them show up in testing, and all of them show up in production with real money attached.
I build and repair these integrations as a Stripe Connect developer — onboarding, split payouts, dispute recovery, and reconciliation — and they're usually part of a wider marketplace or Next.js SaaS build. Card testing, delayed payouts, and Radar for Platforms are a separate layer: Connect fraud and payout risk. If you're weighing charge types or something is already leaking money, send me the details and I'll tell you which model fits and what it would take to get there.