DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Stripe Connect in 45 Minutes: Multi-Vendor Payments Without the Pain

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
}
Enter fullscreen mode Exit fullscreen mode

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 },
  })
}
Enter fullscreen mode Exit fullscreen mode

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 })
}
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

Production Checklist

  • [ ] Test with Stripe test Express accounts in test mode
  • [ ] Check charges_enabled + payouts_enabled before allowing vendor sales
  • [ ] Handle account.updated webhook 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:

Top comments (0)