Stripe Connect is the most powerful and most confusing part of Stripe. Here's the version that covers the real decisions.
Which Account Type?
Express ā vendor onboards via Stripe-hosted flow, you keep control. Right for 95% of multi-vendor SaaS.
Standard ā vendor manages their own full Stripe account. Stripe handles KYC.
Custom ā you own everything including KYC liability. For fintech only.
Create Express Account + Onboarding Link
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function createConnectAccount(email: string) {
const account = await stripe.accounts.create({
type: 'express',
email,
capabilities: { card_payments: { requested: true }, transfers: { requested: true } },
})
return account.id
}
export async function getOnboardingLink(accountId: string, origin: string) {
const link = await stripe.accountLinks.create({
account: accountId,
refresh_url: `${origin}/vendor/onboarding/refresh`,
return_url: `${origin}/vendor/onboarding/complete`,
type: 'account_onboarding',
})
return link.url
}
Split Payments with Destination Charges
export async function chargeWithSplit({
amount, currency, customerId, vendorAccountId, platformFeePercent = 0.1,
}: {
amount: number; currency: string; customerId: string;
vendorAccountId: string; platformFeePercent?: number;
}) {
const applicationFee = Math.round(amount * platformFeePercent)
return stripe.paymentIntents.create({
amount, currency, customer: customerId,
application_fee_amount: applicationFee,
transfer_data: { destination: vendorAccountId },
})
}
application_fee_amount stays with your platform. The rest goes to the vendor automatically.
Connect Webhooks
Register under Connect > Webhooks in Stripe Dashboard ā different from regular webhooks.
export async function POST(req: NextRequest) {
const body = await req.text()
const sig = req.headers.get('stripe-signature')!
const connectAccount = req.headers.get('stripe-account')
const secret = connectAccount
? process.env.STRIPE_CONNECT_WEBHOOK_SECRET!
: process.env.STRIPE_WEBHOOK_SECRET!
const event = stripe.webhooks.constructEvent(body, sig, secret)
if (connectAccount) {
if (event.type === 'account.updated') {
const acct = event.data.object as Stripe.Account
if (acct.charges_enabled && acct.payouts_enabled) {
await db.vendor.update({
where: { stripeAccountId: connectAccount },
data: { status: 'active' },
})
}
}
}
return NextResponse.json({ received: true })
}
Vendor Balance Check
export async function getVendorBalance(accountId: string) {
const balance = await stripe.balance.retrieve({ stripeAccount: accountId })
return {
available: balance.available.reduce((s, b) => s + b.amount, 0) / 100,
pending: balance.pending.reduce((s, b) => s + b.amount, 0) / 100,
}
}
Production Checklist
- [ ] Test with Stripe test Express accounts in test mode
- [ ] Check
charges_enabled+payouts_enabledbefore allowing vendor sales - [ ] Handle
account.updatedwebhook for onboarding completion - [ ] Separate webhook secrets: Connect vs platform
- [ ] Platform fee on every charge
Express account Connect ships in a day. KYC is handled by Stripe. Payouts are automatic. The only complexity is the webhook split ā and now you have that.
Ship Faster With AI
If you're building a SaaS and want to skip the boilerplate, check out whoffagents.com:
- AI SaaS Starter Kit ($99) ā Next.js + Stripe + Auth + Supabase
- Ship Fast Skill Pack ($49) ā Claude Code skills for solo founders
- MCP Security Scanner ($29) ā Audit your MCP server before it goes live












