Subchapter 1.2
references/charge-patterns.mdMarkdown11 KBView on GitHub
Connect offers three ways to create charges involving connected accounts. The charge pattern determines who is the merchant of record, how funds flow, and how fees and refunds work.
| Feature | Direct Charges | Destination Charges | Separate Charges & Transfers |
|---|---|---|---|
| Merchant of record | Connected account | Platform | Platform |
| Payment created on | Connected account | Platform account | Platform account |
| Statement descriptor | Connected account’s | Platform’s (can set connected account’s) | Platform’s |
| Platform fee | application_fee_amount | application_fee_amount or calculate using transfer_data.amount | Manual calculation |
| Refund source | Connected account’s balance | Platform’s balance | Platform’s balance |
| Multi-seller split | No (one seller per charge) | No (one destination per charge) | Yes (multiple transfers) |
| Account requirements | Most v2 configs — see BLOCKED combinations in the controller compatibility note below; the only charge type safe with losses_collector: 'stripe' | Requires losses_collector: 'application' | Requires losses_collector: 'application' |
| Complexity | Low | Low | High |
| Best for | SaaS, seller-owned transactions | Marketplaces, on-demand | Multi-seller carts, complex splits |
Controller Property Compatibility: Works with most controller configurations, but NOT all. BLOCKED combinations for direct charges include:
fees_collector: 'stripe' + losses_collector: 'application'(full or none dashboard), and Express dashboard configs other thanapplication/application. This is the only charge type safe withlosses_collector: 'stripe'. If the platform wants Stripe to own losses, direct charges are the only option.
The charge is created directly on the connected account. The connected account is the merchant of record — their name appears on the customer’s bank statement. The platform collects an application fee.
// Backend: Create PaymentIntent on connected account
const paymentIntent = await stripe.paymentIntents.create({
amount: 10000, // $100.00
currency: 'usd',
application_fee_amount: 1500, // $15.00 platform fee
metadata: {
orderId: 'order_123',
},
}, {
stripeAccount: 'acct_connected_account_id', // Key: stripeAccount header
});
// Return client_secret to frontend
res.json({ clientSecret: paymentIntent.client_secret });// Must initialize Stripe with connected account
const stripe = await loadStripe('pk_test_...', {
stripeAccount: 'acct_connected_account_id',
});
// Then confirm payment as usual
const result = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: 'https://yoursite.com/success',
},
});Customer pays $100
→ $100 lands in connected account's balance
→ $15 application fee transferred to platform
→ Connected account keeps $85// Refund comes from connected account's balance
const refund = await stripe.refunds.create({
charge: 'ch_xxx',
// Optionally refund the application fee too:
refund_application_fee: true,
}, {
stripeAccount: 'acct_connected_account_id',
});Legacy mapping note (external docs terms): Stripe docs still reference legacy v1 naming (
standard,express,custom) and legacy fee-payer behaviors (application_express,application_custom) for older accounts. For migration mapping to Accounts v2 dimensions, see the “Legacy migration note” section in the account-types reference.
Controller Property Compatibility: REQUIRES
losses_collector: 'application'. Using destination charges withlosses_collector: 'stripe'creates a liability-model mismatch for this charge flow. Seecompatibility-matrix.mdfor details.
The charge is created on the platform’s account. The platform is the merchant of record. Funds are automatically transferred to the connected account using transfer_data. This is a common pattern for marketplaces.
// Backend: Create PaymentIntent on platform account
const paymentIntent = await stripe.paymentIntents.create({
amount: 10000, // $100.00
currency: 'usd',
application_fee_amount: 1500, // $15.00 collected; platform net = $15.00 − Stripe processing fees
transfer_data: {
destination: 'acct_connected_account_id', // Funds go here
},
metadata: {
bookingId: 'booking_123',
riderId: 'user_456',
operatorId: 'user_789',
},
});
// Return client_secret to frontend
res.json({ clientSecret: paymentIntent.client_secret });const paymentIntent = await stripe.paymentIntents.create({
amount: 10000, // $100.00
currency: 'usd',
transfer_data: {
destination: 'acct_connected_account_id',
amount: 8500, // $85.00 goes to connected account (platform keeps $15)
},
});// Initialize Stripe with platform's publishable key (no stripeAccount needed)
const stripe = await loadStripe('pk_test_platform_key');
const result = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: 'https://yoursite.com/success',
},
});Customer pays $100
→ $100 lands in platform's balance
→ $85 automatically transferred to connected account
→ Platform nets $15 (application_fee_amount) − Stripe processing fees// Refund comes from platform's balance
const refund = await stripe.refunds.create({
payment_intent: 'pi_xxx',
// Optionally:
reverse_transfer: true, // Claw back from connected account
refund_application_fee: true, // Refund the platform fee too
});Not covered by this guide.
on_behalf_ofis an advanced variant that changes the merchant of record to the connected account while the charge lives on the platform. It has narrow use cases and significant complexity.If your integration requires
on_behalf_of, consult the Stripe Connect documentation (opens in a new tab) or contact Stripe sales (opens in a new tab).Do NOT use
on_behalf_offor marketplace use cases — the platform should be the merchant of record. Use regular destination charges instead.
Controller Property Compatibility: REQUIRES
losses_collector: 'application'. Same negative balance liability issue as destination charges — using separate charges and transfers withlosses_collector: 'stripe'means the platform actually carries the losses despite the configuration. Seecompatibility-matrix.mdfor details.
The charge and transfer are separate API calls. This gives maximum flexibility — you can split a single payment across multiple connected accounts, delay transfers, or create complex fee structures.
// Step 1: Create PaymentIntent (no transfer_data)
const paymentIntent = await stripe.paymentIntents.create({
amount: 10000, // $100.00
currency: 'usd',
metadata: {
orderId: 'order_123',
},
});
// Step 2: After payment_intent.succeeded webhook fires — latest_charge is null
// at creation time and only populated on the confirmed PaymentIntent from the event
// IMPORTANT: Always verify the webhook signature before processing event data.
// See https://stripe.com/docs/webhooks/signatures for verification steps.
const confirmedIntent = event.data.object; // payment_intent.succeeded payload
const transfer = await stripe.transfers.create({
amount: 8500, // $85.00 to connected account
currency: 'usd',
destination: 'acct_connected_account_id',
source_transaction: confirmedIntent.latest_charge, // charge ID from confirmed PaymentIntent
metadata: {
orderId: 'order_123',
},
});// One payment, multiple sellers (for example, a multi-seller cart)
await stripe.paymentIntents.create({
amount: 25000, // $250.00 total
currency: 'usd',
});
// After payment_intent.succeeded webhook fires — latest_charge is null at creation time.
// IMPORTANT: Always verify the webhook signature before processing event data.
// See https://stripe.com/docs/webhooks/signatures for verification steps.
const confirmedIntent = event.data.object; // payment_intent.succeeded payload
const chargeId = confirmedIntent.latest_charge;
// Transfer to seller A
await stripe.transfers.create({
amount: 8000,
currency: 'usd',
destination: 'acct_seller_a',
source_transaction: chargeId,
});
// Transfer to seller B
await stripe.transfers.create({
amount: 12000,
currency: 'usd',
destination: 'acct_seller_b',
source_transaction: chargeId,
});
// Platform keeps $50 (25000 - 8000 - 12000 = 5000)Customer pays $250
→ $250 lands in platform's balance
→ Platform creates transfer: $80 to Seller A
→ Platform creates transfer: $120 to Seller B
→ Platform keeps $50// Refund the charge
const refund = await stripe.refunds.create({
charge: 'ch_xxx',
});
// Manually reverse transfers
await stripe.transfers.createReversal('tr_seller_a', {
amount: 8000,
});
await stripe.transfers.createReversal('tr_seller_b', {
amount: 12000,
});Is there one seller per transaction?
├── Yes → Does the platform need to hold funds before releasing to the seller?
│ ├── Yes (hold-and-release or delivery confirmation) → SEPARATE CHARGES & TRANSFERS
│ └── No → Is the seller the merchant of record?
│ ├── Yes → DIRECT CHARGES
│ └── No → DESTINATION CHARGES ← Common marketplace default
└── No (multiple sellers) → SEPARATE CHARGES & TRANSFERSQuick rules: