Stripe Connect for Marketplaces: Split Payments, Onboarding, and Payouts
Build a Stripe Connect marketplace with split payments, seller onboarding, platform fees, and automated payouts. Complete TypeScript implementation with webhook
Stripe Connect for Marketplaces: Split Payments, Onboarding, and Payouts
Marketplaces โ platforms where buyers pay sellers through your product โ have non-trivial payment infrastructure requirements. You need to collect money from buyers, split it between sellers and yourself, handle failed payouts, manage compliance across jurisdictions, and onboard sellers without requiring them to have existing payment accounts.
Stripe Connect is the most complete managed solution for this. This guide covers the complete implementation: account types, onboarding flows, payment splitting, webhook handling, and the failure cases most tutorials skip.
Stripe Connect Account Types
Three account types exist, each with different tradeoffs:
| Type | UX Control | KYC Responsibility | Stripe Branding Visible | Best For |
|---|---|---|---|---|
| Standard | Seller uses Stripe dashboard | Stripe handles | Yes | Platforms where sellers already use Stripe |
| Express | Stripe-hosted onboarding | Stripe handles | Minimal | Most marketplaces โ fastest to ship |
| Custom | Fully white-labeled | Platform responsible | No | Enterprise, financial services |
Recommendation for most marketplaces: Express. You get Stripe-hosted KYC/KYB onboarding (they handle identity verification, bank account collection, and compliance), your platform brand is visible, and sellers use a simplified Stripe Express dashboard rather than the full Stripe interface.
Step 1: Create a Connected Account and Onboarding Link
// lib/stripe.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18',
typescript: true,
});
export { stripe };
// api/sellers/onboard.ts โ Create Express account + onboarding link
export async function POST(request: Request) {
const { sellerId, email, businessType } = await request.json();
// 1. Check if seller already has a Stripe account
const seller = await db.seller.findUnique({ where: { id: sellerId } });
let stripeAccountId = seller?.stripeAccountId;
if (!stripeAccountId) {
// 2. Create Express connected account
const account = await stripe.accounts.create({
type: 'express',
email,
business_type: businessType ?? 'individual', // 'individual' | 'company'
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
settings: {
payouts: {
schedule: {
interval: 'weekly', // 'daily' | 'weekly' | 'monthly'
weekly_anchor: 'monday',
},
},
},
metadata: { sellerId },
});
stripeAccountId = account.id;
// 3. Save to DB
await db.seller.update({
where: { id: sellerId },
data: {
stripeAccountId,
stripeOnboardingStatus: 'pending',
},
});
}
// 4. Generate onboarding link (expires after ~1 hour)
const accountLink = await stripe.accountLinks.create({
account: stripeAccountId,
refresh_url: `${process.env.APP_URL}/sellers/onboard/refresh`,
return_url: `${process.env.APP_URL}/sellers/onboard/complete`,
type: 'account_onboarding',
});
return Response.json({ url: accountLink.url });
}
The seller clicks the link, completes Stripe's hosted flow (identity, bank account), and lands on your return_url. Check their account status there:
// api/sellers/onboard/complete.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const sellerId = searchParams.get('sellerId')!;
const seller = await db.seller.findUnique({ where: { id: sellerId } });
const account = await stripe.accounts.retrieve(seller!.stripeAccountId!);
const isEnabled =
account.charges_enabled &&
account.payouts_enabled &&
account.details_submitted;
await db.seller.update({
where: { id: sellerId },
data: {
stripeOnboardingStatus: isEnabled ? 'complete' : 'pending',
stripeChargesEnabled: account.charges_enabled,
stripePayoutsEnabled: account.payouts_enabled,
},
});
return Response.redirect(
isEnabled
? `${process.env.APP_URL}/sellers/dashboard`
: `${process.env.APP_URL}/sellers/onboard/incomplete`
);
}
๐ค Can This Strategy Be Automated?
In 2026, top traders run custom EAs โ not manual charts. We build MT4/MT5 Expert Advisors that execute your exact strategy 24/7, pass prop firm challenges, and eliminate emotional decisions.
- Runs 24/7 โ no screen time, no missed entries
- Prop-firm compliant (FTMO, MFF, TFT drawdown rules)
- MyFXBook-verified backtest results included
- From strategy brief to live EA in 2โ4 weeks
Step 2: Collecting Payment with Automatic Split
When a buyer pays, create a PaymentIntent that automatically routes the seller's share to their account:
// api/orders/create.ts
interface CreateOrderBody {
buyerId: string;
sellerId: string;
items: Array<{ productId: string; quantity: number; unitPrice: number }>;
}
export async function POST(request: Request) {
const body: CreateOrderBody = await request.json();
const seller = await db.seller.findUnique({ where: { id: body.sellerId } });
if (!seller?.stripeAccountId || !seller.stripeChargesEnabled) {
return Response.json({ error: 'Seller not ready to accept payments' }, { status: 400 });
}
// Calculate amounts (in cents)
const subtotalCents = body.items.reduce(
(sum, item) => sum + item.unitPrice * item.quantity,
0
);
const platformFeeCents = Math.round(subtotalCents * 0.05); // 5% platform fee
const sellerShareCents = subtotalCents - platformFeeCents;
// Create order record
const order = await db.order.create({
data: {
buyerId: body.buyerId,
sellerId: body.sellerId,
subtotalCents,
platformFeeCents,
sellerShareCents,
status: 'PENDING_PAYMENT',
items: { create: body.items },
},
});
// Create PaymentIntent with destination charge
const paymentIntent = await stripe.paymentIntents.create({
amount: subtotalCents,
currency: 'usd',
payment_method_types: ['card'],
application_fee_amount: platformFeeCents, // Platform keeps this
transfer_data: {
destination: seller.stripeAccountId, // Seller receives the rest
},
metadata: {
orderId: order.id,
buyerId: body.buyerId,
sellerId: body.sellerId,
},
description: `Order ${order.id} โ ${body.items.length} item(s)`,
});
// Save PaymentIntent ID
await db.order.update({
where: { id: order.id },
data: { stripePaymentIntentId: paymentIntent.id },
});
return Response.json({
clientSecret: paymentIntent.client_secret,
orderId: order.id,
});
}
Step 3: Handling Webhooks
Webhooks are how Stripe tells you when payments succeed, fail, or sellers update their accounts.
// api/webhooks/stripe.ts
import { stripe } from '@/lib/stripe';
export async function POST(request: Request) {
const rawBody = await request.arrayBuffer();
const sig = request.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
Buffer.from(rawBody),
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return Response.json({ error: 'Invalid signature' }, { status: 400 });
}
// Idempotency โ skip if already processed
const existing = await db.processedStripeEvent.findUnique({
where: { stripeEventId: event.id },
});
if (existing) return Response.json({ received: true });
await db.processedStripeEvent.create({
data: { stripeEventId: event.id, type: event.type },
});
switch (event.type) {
case 'payment_intent.succeeded': {
const pi = event.data.object as Stripe.PaymentIntent;
await db.order.update({
where: { stripePaymentIntentId: pi.id },
data: { status: 'PAID', paidAt: new Date() },
});
await notifySellerOfNewOrder(pi.metadata.orderId);
break;
}
case 'payment_intent.payment_failed': {
const pi = event.data.object as Stripe.PaymentIntent;
await db.order.update({
where: { stripePaymentIntentId: pi.id },
data: {
status: 'PAYMENT_FAILED',
failureReason: pi.last_payment_error?.message,
},
});
break;
}
case 'account.updated': {
// Seller updated their Stripe account (completed onboarding, added bank, etc.)
const account = event.data.object as Stripe.Account;
const seller = await db.seller.findFirst({
where: { stripeAccountId: account.id },
});
if (seller) {
await db.seller.update({
where: { id: seller.id },
data: {
stripeChargesEnabled: account.charges_enabled,
stripePayoutsEnabled: account.payouts_enabled,
stripeOnboardingStatus:
account.charges_enabled && account.payouts_enabled
? 'complete'
: 'pending',
},
});
}
break;
}
case 'payout.paid': {
const payout = event.data.object as Stripe.Payout;
// Log successful payout to seller's bank
await db.sellerPayout.updateMany({
where: { stripePayoutId: payout.id },
data: { status: 'PAID', paidAt: new Date() },
});
break;
}
case 'payout.failed': {
const payout = event.data.object as Stripe.Payout;
await handleFailedPayout(payout); // Alert seller, update status
break;
}
}
return Response.json({ received: true });
}
๐ Stop Trading Manually โ Let AI Do It
While you sleep, your EA keeps working. Viprasol builds prop-firm-compliant Expert Advisors with strict risk management, real backtests, and live deployment support.
- No rule violations โ daily drawdown, max drawdown, consistency rules built in
- Covers MT4, MT5, cTrader, and Python-based algos
- 5.0โ Upwork record โ 100% job success rate
- Free strategy consultation before we write a single line
Step 4: Handling Refunds
Refunds on marketplace payments require coordination between platform fee reversal and seller balance:
// api/orders/refund.ts
export async function POST(request: Request) {
const { orderId, reason } = await request.json();
const order = await db.order.findUnique({
where: { id: orderId },
include: { seller: true },
});
if (!order?.stripePaymentIntentId) {
return Response.json({ error: 'Order not found or not paid' }, { status: 400 });
}
// Full refund โ reverses both platform fee and seller transfer
const refund = await stripe.refunds.create({
payment_intent: order.stripePaymentIntentId,
reason: reason ?? 'requested_by_customer',
refund_application_fee: true, // Refund the platform fee too
reverse_transfer: true, // Pull back money from seller's balance
});
await db.order.update({
where: { id: orderId },
data: {
status: 'REFUNDED',
stripeRefundId: refund.id,
refundedAt: new Date(),
},
});
return Response.json({ refundId: refund.id, status: refund.status });
}
Compliance and Risk Considerations
| Area | What You're Responsible For |
|---|---|
| KYC/KYB | Express: Stripe handles. Custom: You must implement. |
| 1099-K reporting | Stripe files for sellers who exceed IRS thresholds ($600+ in 2026) |
| Chargeback liability | Destination charges: seller bears chargeback risk, not platform |
| Restricted businesses | You must not onboard sellers in prohibited categories (gambling, adult content in some regions, etc.) |
| International payouts | Stripe handles currency conversion; you set payout currency per account |
| Funds holding | Stripe holds funds 7 days by default for new accounts before releasing to sellers |
Platform Fee Strategies
| Model | Implementation | Best For |
|---|---|---|
| Flat percentage | application_fee_amount = amount * 0.05 | Simplest, easiest to explain |
| Tiered by volume | Fetch seller's MTD volume, apply tier rate | Incentivizes high-volume sellers |
| Per-transaction + % | fee = 50 + amount * 0.025 | Covers Stripe's base cost |
| Category-based | Different rates per product category | Marketplaces with variable-margin categories |
Cost Model
| Component | Cost |
|---|---|
| Stripe processing | 2.9% + $0.30 per transaction |
| Stripe Connect (Express) | 0.25% per payout (max $2/payout) |
| Instant payouts (optional) | 1% per instant payout |
| Stripe Radar (fraud) | $0.05/transaction (paid plan) |
| International cards | +1.5% |
Platform economics example (5% platform fee, $100 order):
- Buyer pays: $100.00
- Stripe fee: $3.20 (2.9% + $0.30)
- Platform fee: $5.00
- Stripe Connect fee: $0.25
- Net to platform: $1.55
- Net to seller: $91.25
Build vs Buy
| Approach | Timeline | Cost | Maintenance |
|---|---|---|---|
| Stripe Connect Express (this guide) | 4โ8 weeks | $15,000โ40,000 dev | Low |
| Stripe Connect Custom | 8โ16 weeks | $40,000โ100,000 dev | Medium |
| Full custom (own bank relationships) | 6โ18 months | $200,000โ500,000+ | Very High |
For 99% of marketplaces, Stripe Connect Express is the right choice.
Working With Viprasol
We've built marketplace payment infrastructure for multi-vendor e-commerce platforms, service marketplaces, and gig economy apps. Our implementations include seller onboarding, split payment flows, payout dashboards, dispute handling, and regulatory compliance tooling.
โ Talk to our payments team about your marketplace architecture.
See Also
- Payment Gateway Integration โ Stripe basics for standard checkout
- SaaS Pricing Strategy โ platform fee models and revenue optimization
- API Security Best Practices โ securing payment APIs
- Webhook Design Patterns โ reliable Stripe webhook processing
- Fintech Software Development โ financial application development
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Ready to Automate Your Trading?
Get a custom Expert Advisor built by professionals with verified MyFXBook results.
Free consultation โข No commitment โข Response within 24 hours
Need a custom EA or trading bot built?
We specialise in MT4/MT5 Expert Advisor development โ prop-firm compliant, forward-tested before live, MyFXBook verifiable. 5.0โ Upwork, 100% Job Success, 100+ projects shipped.