Direct, destination, and separate charges: the revenue consequences
Same sale, same refund, three different platform outcomes: direct, destination, and separate charges compared strictly by where fees and refund costs land.
Direct, destination, and separate charges: the revenue consequences
Same sale, same refund: where do Stripe fees, refund debits, and dispute debits land under direct, destination, and separate charges? This comparison answers that for founders, engineers, and finance teams running Connect platforms, strictly by following the money through each pattern.
Three patterns, one paragraph each
All three patterns are documented together in Stripe's Connect charges overview. The differences that matter to revenue are differences of location: of the charge object, of the balances that get debited, and of which objects exist at all.
Direct charges are created on the connected account itself, passed through the Stripe-Account header. The payment lands in the connected account's balance, an application_fee_amount skims the platform's cut into the platform balance, and refunds and disputes debit the connected account. The seller effectively runs their own payments with your software attached.
Destination charges are created on the platform with transfer_data[destination]. Stripe immediately transfers the seller's portion onward while the platform balance absorbs Stripe's processing fees, refunds, and disputes. One charge object, one transfer, and a money path that lives mostly on the platform side.
Separate charges and transfers decouple those steps entirely. The charge sits on the platform, and Transfers move funds to one or more connected accounts whenever you choose. The platform balance again absorbs fees, refunds, and disputes, but there is no application-fee object and no automatic linkage between any refund and any transfer.
Where every cost lands
The matrix below pins down where each cost type lands under each pattern, compiled from Stripe's documentation on charges and refunds by pattern and disputes on Connect:
| Question | Direct | Destination | Separate |
|---|---|---|---|
| Processing fee reduces whose funds | The connected account's (the charge nets down to them) | Platform balance | Platform balance |
ApplicationFee object created | Yes, asynchronously by default; application_fee.created announces it | Yes | No — the platform collects by transferring less |
| Refund debited from | Connected account balance | Platform balance | Platform balance |
| Refund when the paying balance is short | Refund status pending until funded (cards) | Refund proceeds; if a reversal was requested and the seller lacks funds, the API errors instead | Refund proceeds; transfers are unaffected either way |
| Disputed amount debited from | Connected account | Platform balance | Platform balance |
| Dispute fee paid by | Configuration-dependent: controller.fees.payer / defaults.responsibilities.fees_collector set to application means the platform pays; account / stripe variants mean the connected account pays | Platform | Platform |
| Post-refund recovery mechanism | refund_application_fee=true, or the separate fee-refund API afterward | Transfer reversal via reverse_transfer=true, or Transfer Reversals after the fact | Manual POST /v1/transfers/{id}/reversals, or reduced future transfers |
Two rows deserve emphasis. First, the processing-fee row: under destination and separate charges the platform's balance pays Stripe, so when a refund happens the fee is gone from the platform's pocket; under direct charges it was gone from the seller's. Second, the dispute rows: with destination and separate patterns Stripe debits both the disputed amount and the dispute fee from the platform balance, and making the seller cover it is a manual transfer reversal — including cross-border cases, where Stripe advises waiting until a dispute is lost before recovering the transfer.
The ApplicationFee row matters for reporting as much as for mechanics. The object carries amount, amount_refunded, currency, account, balance_transaction, and charge fields (ApplicationFee object) — everything a reconciliation needs, except existence itself under separate charges. On direct charges the fee object appears asynchronously by default, announced by application_fee.created, though expanding application_fee at charge time forces synchronous creation. One currency nuance affects destination platforms collecting fees across borders: with application_fee_amount, the platform receives the fee in the connected account's settlement currency, whereas retaining funds via transfer_data[amount] keeps them in the platform's default currency.
Which refund flags exist in each pattern
The Refunds API takes two Connect-relevant booleans (Refunds create), and their applicability depends entirely on the pattern:
| Parameter | Direct | Destination | Separate |
|---|---|---|---|
refund_application_fee | Applies: full refund returns the full fee; partial refund returns proportional | Applies: same full-to-full, partial-to-proportional behavior | Meaningless — no application fee exists to return |
reverse_transfer | Nothing to reverse — the charge creates no transfer | Applies: full refund reverses the entire transfer; partial reverses proportionally | Meaningless — reversing transfers is manual work |
Both parameters carry a creator restriction worth knowing: an application fee can be refunded only by the application that created the charge, and the same holds for transfer reversals — your platform, in both cases. Partial refunds inherit full-to-full and partial-to-proportional proportionality wherever the flags apply, which is precisely what makes expected-value audit formulas deterministic instead of judgment calls. Destination-specific defaults, including the default that leaves transferred funds with the seller, are covered step by step in the destination-charge refund explainer.
The identical business test
Numbers beat adjectives, so run one business sold three ways. Fixed assumptions, all adjustable:
- US platform, USD; standard US card pricing of 2.9% + $0.30, giving $3.20 processing on every $100.00 charge.
- Charge amount: $100.00, or 10,000 cents.
- Platform fee: $10.00 wherever the pattern supports one (
application_fee_amount=1000on direct and destination). - Separate pattern: the platform transfers $70.00 to the seller and collects $26.80 implicitly.
- Recommended refund settings per pattern: direct sets
refund_application_fee=true; destination setsreverse_transfer=trueand keeps the fee (destination-charge defaults); separate performs a manual reversal of the $70.00 transfer. - Recommended encodes one policy: buyer made whole, seller returns the sale's consideration, platform keeps its fee as revenue.
After the sale, before any refund:
| Position | Direct | Destination | Separate |
|---|---|---|---|
| Platform | +$10.00 | +$6.80 | +$26.80 |
| Seller | +$86.80 | +$90.00 | +$70.00 |
| Stripe | +$3.20 | +$3.20 | +$3.20 |
| Buyer | −$100.00 | −$100.00 | −$100.00 |
Every column sums to zero: 10.00 + 86.80 + 3.20 − 100.00 = 0; 6.80 + 90.00 + 3.20 − 100.00 = 0; 26.80 + 70.00 + 3.20 − 100.00 = 0. The cells themselves derive in three lines:
Direct: seller $100.00 - $3.20 - $10.00 = $86.80 platform takes the fee object's $10.00
Destination: platform $96.80 - $90.00 = $6.80 net charge minus the $90.00 transfer
Separate: platform $96.80 - $70.00 = $26.80 net charge minus the smaller transfer
Now the identical full refund under each pattern's recommended settings. The arithmetic per column:
Direct: seller $86.80 - $100.00 + $10.00 = -$3.20
platform $10.00 - $10.00 = $0.00
Destination: platform $6.80 - $100.00 + $90.00 = -$3.20
seller $90.00 - $90.00 = $0.00
Separate: platform $26.80 - $100.00 + $70.00 = -$3.20
seller $70.00 - $70.00 = $0.00
Final positions side by side:
| After the full refund | Direct | Destination | Separate (reversal works) |
|---|---|---|---|
| Platform | $0.00 | −$3.20 | −$3.20 |
| Seller | −$3.20 | $0.00 | $0.00 |
| Stripe | +$3.20 | +$3.20 | +$3.20 |
| Buyer | $0.00 | $0.00 | $0.00 |
Read the Stripe row first: plus $3.20 everywhere, because "Stripe's processing fees from the original transaction aren't returned" (Refunds). Who funds that constant is what differs: the seller under direct charges, the platform under the other two. Read the buyer row second: zero everywhere, because refund routing never touches the buyer's outcome.
Now break the separate pattern deliberately. A reversal succeeds only if the connected account's available balance covers it, and refunding a separate charge has no impact on any associated transfers (separate charges and transfers). Suppose the seller has already drawn payouts and their available balance cannot cover $70.00:
Separate, reversal unavailable:
platform $26.80 - $100.00 = -$73.20
seller unchanged = +$70.00
| After the full refund | Separate (seller cannot cover) |
|---|---|
| Platform | −$73.20 |
| Seller | +$70.00 |
| Stripe | +$3.20 |
| Buyer | $0.00 |
Sum check: −73.20 + 70.00 + 3.20 = 0. The platform's −$73.20 position persists until recovery happens — reversing later once the seller re-accumulates balance, or trimming future transfers until the shortfall is repaid. Both routes are procedures rather than flags (separate-transfer recovery playbook, netting against future transfers).
Four columns, one identical business, four different platform outcomes: $0.00, −$3.20, −$3.20, −$73.20. The charge type decided all four before anyone touched a refund endpoint.
The same test extends to disputes without new arithmetic. Under destination and separate patterns the disputed amount plus the dispute fee debit the platform balance, and recovery from the seller is again a manual transfer reversal; under direct charges the disputed amount debits the seller's balance while the dispute fee follows the fee-payer configuration described earlier (disputes on Connect). Refund handling and dispute handling therefore reward the same decision: whichever pattern keeps the money path on ledgers you can query is also the pattern whose losses you can chase.
Visibility asymmetries
Where the money lands determines what you can see without extra queries.
Direct charges are the extreme case, documented by Stripe as a visibility limitation: transaction objects such as PaymentIntents and Charges exist on the connected account rather than the platform, and retrieving them requires per-account queries through the Stripe-Account header (direct charges, Platform visibility limitations). Your fee objects remain visible platform-side — Collected fees in the Dashboard, listable via the API, queryable in Sigma — so revenue reads cleanly while the underlying payments must be gathered account by account.
Destination charges produce the cleanest platform-side picture. The charge, its application_fee_amount, the transfer, and any reversals all sit in ledgers your platform key reads directly, so one query family covers the whole money path.
Separate charges hide revenue in plain sight. No fee object records your take anywhere on the platform ledger; platform revenue equals the charge net minus the sum of transfers, a derived quantity that exists only as balance history entries. Auditing it means computing that difference per charge — the same join-and-diff discipline that leak detection needs, applied here to ordinary revenue.
Reporting products inherit these shapes as well. Stripe's report types itemize balance changes and payouts for reconciliation (Stripe reports), and Sigma queries run over the same per-pattern objects (Sigma schema). Where an object class does not exist for a pattern, its rows appear in neither product — the separate pattern's absent fee objects being the clearest case: nothing to query, so nothing reported, even though the money moved.
Operational consequences
The consequences stack by pattern:
| Dimension | Direct | Destination | Separate |
|---|---|---|---|
| Reconciliation effort | Per-account enumeration; asynchronous fee objects to await | Lowest: one platform-side ledger tells the story | Highest: pair charges to transfers yourself (transfer_group helps) |
| Refund-time tooling | Set refund_application_fee; handle pending refunds on thin balances | Set flags deliberately; handle the reversal-request error when sellers lack funds | Build reversal scheduling, insufficient-funds handling, and retry logic |
| Dispute recovery | Largely the seller's, per fee-payer configuration | Manual reversal after a lost dispute | Manual reversal after a lost dispute |
| Fleet-wide audit complexity | Grows with connected-account count | One ledger | One ledger, but implicit revenue must be computed |
Two engineering notes apply regardless of pattern. Reversals issued in batches deserve idempotency keys, which Stripe honors for 24 hours (Idempotency), so a retried script cannot double-pull from a seller. And failure modes differ sharply by pattern: the direct pattern's worst case is a pending refund that resolves itself once the seller's balance funds it, destination hands you an explicit error when a requested reversal cannot be funded, and separate gives you silence — nothing fails, because nothing was ever linked.
Automation built on events should also assume imperfect transport. Webhook delivery is at-least-once with retries stretching up to about three days; signatures require verification, and handlers need to be idempotent (Webhooks). Tooling that treats an event as an instruction rather than a notification to reconcile against the ledger will eventually act on stale or duplicated data — harmless in a dashboard, expensive in a funds-moving script.
Choosing deliberately
Match the pattern to the business shape; each mapping below is mechanical rather than preferential:
- Sellers must appear as the merchant — their statement descriptor, their settlement country, their own Dashboard relationship — and you sell software on top of their volume: direct charges.
- You operate the storefront and want the simplest seller experience with platform-side books that reconcile themselves: destination charges.
- Money must split several ways, be held back, be timed independently of the charge, or reach multiple recipients: separate charges and transfers, accepting the manual recovery burden computed above.
- Marketplaces reselling other businesses' goods usually fit the destination shape (notes for marketplaces); platforms billing SaaS subscriptions atop seller volume usually fit the direct shape (notes for SaaS resellers).
The choice is per charge, not permanent: every new charge can pick differently. But historical objects stay as created, so whichever refund mechanics you build for a given pattern will follow that pattern's ledger for as long as those charges exist. One closing discipline ties the comparison together: the refund-time defaults under every pattern were chosen by someone. Confirming that the someone was you — in writing, per pattern, with the flag settings recorded next to it — is most of the revenue work this article describes.
Frequently asked questions
Do buyers experience any difference between the three?
No. The refund reaches the original payment method identically under all three patterns; routing differences live entirely on the platform-seller side. Only funding behavior differs, such as a direct-charge refund sitting in pending status until the seller's balance covers it.
Can one platform mix patterns?
Yes, per charge. Mixing is common — destination for marketplace orders, separate for payout-style flows, direct where sellers keep their own Stripe relationship — but every additional pattern widens the reconciliation surface: more ledger shapes to parse, more flag matrices to apply correctly, and more recovery paths to maintain and test against real error cases.
Who ultimately absorbs the $3.20 when a sale is refunded?
Nobody gets it back, since Stripe's processing fees are not returned on refunds. Economically it lands on whoever's balance funded the original charge: the seller under direct charges, the platform under destination and separate. The worked tables in this article make that split explicit, cell by cell.
Does on_behalf_of change any of this?
It changes branding, settlement country and currency, and which country's fees apply. It does not change debit locations: for destination and separate charges created with on_behalf_of, the platform balance is still debited for refunds and disputes. Treat on_behalf_of as presentation, not protection.
Is there an application_fee_amount on separate charges?
No. The separate pattern creates no application-fee objects at all; the platform collects its fee by transferring less than the charge amount. Revenue therefore exists only as a derived quantity — charge net minus transfers — visible in balance history rather than in a dedicated object.
Run the 90-day audit
FeeGuard exists because these outcomes diverge silently on every refund and dispute your platform handles. The free audit reads your last 90 days of Connect activity through a restricted, read-only API key and reports every unreversed transfer, unreclaimed application fee, and uncovered dispute loss with the amounts attached. You get the answer first; ongoing monitoring is optional afterward.
FeeGuard is an independent product and is not affiliated with, endorsed by, or sponsored by Stripe, Inc.