Building a marketplace means one thing above all else: money flows through you to other people. The moment a platform splits a payment between itself and a seller, you've left "add a checkout button" territory and entered regulated payments infrastructure. This guide walks through how we architect production Stripe Connect marketplaces in Next.js — the same approach we use for client builds at PolarSoftBD.
What Stripe Connect actually solves
A marketplace has three parties: the buyer, the seller, and you (the platform). Stripe Connect exists to answer the hard questions in that triangle:
- How do sellers get onboarded and KYC-verified without you becoming a compliance company?
- How does a single charge get split between the seller and your platform fee?
- Who pays Stripe's processing fees, and who eats refunds and chargebacks?
- When and how do sellers get paid out?
If you try to fake this with a single Stripe account and manual payouts, you will eventually hit fund-flow rules that shut you down. Connect is the legal and technical rails for doing it correctly.
Step 1 — Choose your account type (this decision is 80% of the architecture)
Connect gives you three seller account flavors. For 90% of marketplaces we build, Express is the right answer:
| | Standard | Express | Custom | |---|---|---|---| | Onboarding UI | Stripe-hosted, full dashboard | Stripe-hosted, light dashboard | You build everything | | KYC burden | Stripe | Stripe | Mostly you | | Branding control | Low | Medium | Total | | Engineering cost | Lowest | Low | Very high | | Best for | SaaS platforms adding payments | Most marketplaces | Large platforms with payments teams |
Express gives you Stripe-managed identity verification and a hosted seller dashboard while keeping the buyer experience 100% yours. (We wrote a deeper comparison in Stripe Connect Express vs Custom Accounts.)
Step 2 — Seller onboarding flow
The onboarding flow in a Next.js App Router project is two server actions and a redirect:
// app/api/connect/onboard/route.ts
import { NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST() {
const seller = await getCurrentSeller(); // your auth
// 1. Create the connected account once, store the id
let accountId = seller.stripeAccountId;
if (!accountId) {
const account = await stripe.accounts.create({
type: "express",
email: seller.email,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
});
accountId = account.id;
await saveSellerAccountId(seller.id, accountId);
}
// 2. Generate a fresh onboarding link (they expire — always mint on demand)
const link = await stripe.accountLinks.create({
account: accountId,
refresh_url: `${process.env.APP_URL}/dashboard/onboarding/refresh`,
return_url: `${process.env.APP_URL}/dashboard/onboarding/complete`,
type: "account_onboarding",
});
return NextResponse.json({ url: link.url });
}
Two production mistakes to avoid here:
- Never store the onboarding URL — account links expire in minutes. Mint a new one every time the seller clicks.
- The
return_urldoes not mean onboarding succeeded. Sellers can abandon halfway. The only source of truth is theaccount.updatedwebhook withcharges_enabled: trueandpayouts_enabled: true.
Step 3 — Splitting payments: destination charges
For most marketplaces we recommend destination charges: the buyer pays your platform, and Stripe automatically transfers the seller's share to their connected account.
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price_data: {
currency: "usd",
unit_amount: 12000, // $120.00
product_data: { name: listing.title },
}, quantity: 1 }],
payment_intent_data: {
application_fee_amount: 1800, // your 15% platform fee
transfer_data: { destination: seller.stripeAccountId },
},
success_url: `${APP_URL}/orders/{CHECKOUT_SESSION_ID}/success`,
cancel_url: `${APP_URL}/listings/${listing.id}`,
});
The application_fee_amount is your revenue. Stripe handles the split, the ledger, and the seller's balance — you never touch the money manually, which keeps you out of money-transmitter territory.
Step 4 — Webhooks are the real backend
Marketplace state lives in webhooks, not in redirect handlers. The minimum set you must handle idempotently:
checkout.session.completed— mark the order paidaccount.updated— flip sellers to "can sell" only whencharges_enabledcharge.refunded/charge.dispute.created— reverse transfers if your policy requires itpayout.paid/payout.failed— surface payout status to sellers
Idempotency matters because Stripe retries. Store the event id and skip duplicates before processing — duplicate checkout.session.completed events creating double orders is the single most common marketplace bug we get hired to fix.
Step 5 — The parts nobody tells you about
A production marketplace also needs: refund flows that reverse the platform fee (or don't — a policy decision), 1099-K tax reporting via Stripe Tax for US sellers, payout schedule configuration, a dispute-handling runbook, and seller balance visibility. Budget as much time for these as for the happy path.
Get it built without the trial-and-error
We've shipped Stripe Connect marketplaces end-to-end — Express onboarding, split payments, webhooks, seller dashboards, refunds, and payout flows. If you'd rather skip three months of Stripe documentation archaeology:
- Stripe integration service → — Connect marketplace integrations start at $400 fixed price
- Book a free 30-minute strategy call → — we'll scope your marketplace and give you a fixed quote
Or keep reading: Usage-Based Billing with Stripe and Building AI Credit Systems for SaaS.