Stripe Connect Marketplace Architecture: Direct vs Express vs Custom
A pragmatic, production-grade guide to choosing and implementing Stripe Connect for marketplaces. Understand Direct, Express, and Custom models, money flows, code, and trade-offs.

Pick the wrong Stripe Connect model and you’ll pay for it twice: once in rework (re-onboarding every seller) and again in compliance risk, support load, and payout failures you can’t unwind. This guide lays out how we architect marketplaces on Stripe Connect, what breaks in production, and when to choose Direct (Standard), Express, or Custom.
If you just need the answer: use Express when you want fast launch and Stripe to carry most compliance overhead; use Custom when you need full brand control, non-trivial KYC flows, or non-standard payout logic; use Direct (Standard) when your sellers are already Stripe-native and should own their dashboards and risk. The money flow style (Destination vs Direct vs Separate charges and transfers) must match your compliance stance and UX.
The three Connect models at a glance
Stripe Connect offers three account types for your sellers ("connected accounts"): Standard (often called Direct), Express, and Custom. The right choice is primarily about control vs. responsibility.
| Dimension | Direct (Standard) | Express | Custom |
|---|---|---|---|
| Seller dashboard | Full Stripe dashboard owned by seller | Lightweight Stripe-hosted dashboard (Express) | No Stripe dashboard; you build everything |
| Onboarding | Stripe-hosted OAuth, minimal build effort | Stripe-hosted onboarding links | Fully custom onboarding UI via Accounts API |
| KYC/Compliance ownership | Seller (Stripe handles for their account) | Shared: Stripe handles verification; platform configures | Platform owns UX, Stripe verifies; platform handles comms/support |
| Payout control | Seller controls | Platform-configured schedules | Full control (you set schedules, methods) |
| Branding control | Low | Medium | High |
| Support burden | Low | Medium | High |
| Feature flexibility (e.g., split fees, special holds) | Low | Medium | Highest |
| PCI scope (typical) | Lower (Stripe-hosted checkout helps) | Lower | Depends on your capture flow; can be higher |
| Time to launch | Fastest | Fast | Slowest |
| Best for | Sellers who want their own Stripe | Classic marketplaces needing speed & some control | Sophisticated platforms, regulated/complex flows |
Two other axes matter: how you charge customers and how you route funds.
- Destination charges: Customer is charged on your platform account; funds routed to a connected account via
transfer_data. You can take an application fee. Common with Express/Custom when the platform wants to manage experience. - Direct charges: Customer is charged on the connected account. Platform can take a fee using application fees. Common with Standard (Direct) because the seller owns the charge.
- Separate charges and transfers: You charge on the platform account, then move funds to connected accounts later. Useful for multi-seller carts, delayed allocation, and complex holds. Requires careful balance/negative balance handling.
Reference marketplace architecture (production-ready)
A pragmatic build that scales to 10–100k sellers without heroics:
- Frontend: React/Next.js or Vue/Nuxt for marketplace UI. Stripe Elements/Payment Element for PCI-light card capture; add Link/Wallets where appropriate.
- Backend: Node.js (NestJS/Fastify) or Go, with strict idempotency, retry-safe design.
- Data: Postgres for orders, payouts, KYC states; Redis for dedupe and webhook idempotency; S3-compatible storage for documents (if you pre-collect before sending to Stripe).
- Queue: BullMQ/Sidekiq/Cloud Tasks for async payouts, reconciliation, and retries.
- Stripe: Connect + PaymentIntents + Transfers + Webhooks. Use Stripe Radar and optional Stripe Identity for verification. Consider Treasury/Issuing only if you truly need them.
- Observability: Structured logs with request IDs, webhook event IDs; metrics on payout failures, verification states, balance health; audit trails for fund movements.
- Security: Least-privilege API keys; separate publishable/secret; route webhooks via a verified endpoint. We often front webhook ingress with an edge firewall and signature verification; see our notes on hardening edge paths in building for the future with Cloudflare.
High-level flow (Express example):
- Seller applies to join → you create a connected account (
type: 'express') and generate an onboarding link. - Seller completes KYC on Stripe-hosted pages → Stripe updates
account.capabilities(e.g.,transfers,card_payments). - Buyer checks out → you create a
PaymentIntenton the platform withtransfer_data[destination]andapplication_fee_amount. - Payment succeeds → platform takes fee; net funds route to seller; you update order state.
- Payouts run on seller schedule → you listen to
payout.*events for reconciliation.
Implementation patterns by model
Direct (Standard)
- Onboarding pattern: OAuth to connect an existing Stripe account. Minimal fields to collect; Stripe handles verification and dashboard.
- Charging pattern: Direct charges. The charge is created on the connected account; you can set
application_fee_amountto monetize. - Pros: Fastest setup; sellers fully own risk and chargebacks; great for sellers that already use Stripe.
- Gotchas: Limited platform visibility and control; refunds/disputes happen on the seller’s account, so your customer support flow must coordinate.
Express
- Onboarding pattern: You create an Express account and send a Stripe-hosted onboarding link. Stripe gathers the right KYC/ownership docs per region.
- Charging pattern: Destination charges (simplest) or Separate charges and transfers (multi-seller cart). You can set fee and destination in one shot.
- Pros: Balanced control vs. compliance; your support team keeps line-of-sight; payouts configurable.
- Gotchas: You still need to model capability states, payout holds, and communicate verification failures to sellers.
Custom
- Onboarding pattern: You collect all seller info in your UI and push to Stripe via the Accounts API. For identity docs, either use Stripe Identity or guide uploads into your flow and then to Stripe.
- Charging pattern: Usually Destination or Separate charges and transfers. You fully own payout schedule logic and communications.
- Pros: Full white-label, unified UX, advanced fee logic, advanced fund holds.
- Gotchas: Highest compliance/support burden; you must build dashboards, tax forms surface, 1099/K-forms orchestration (if applicable), and granular KYC state management.
Money flow choices with code
Below are TypeScript-flavored Node snippets that illustrate common patterns. These are simplified; in production add error handling, retries, and idempotency keys.
// Create an Express connected account and onboarding link
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16' });
export async function createExpressSeller(userId: string) {
const account = await stripe.accounts.create({
type: 'express',
capabilities: { card_payments: { requested: true }, transfers: { requested: true } },
business_type: 'individual', // or 'company' based on your intake
metadata: { userId }
});
const link = await stripe.accountLinks.create({
account: account.id,
refresh_url: 'https://example.com/onboarding/refresh',
return_url: 'https://example.com/onboarding/return',
type: 'account_onboarding'
});
return { accountId: account.id, onboardingUrl: link.url };
}
Destination charges for Express/Custom (platform takes a fee, funds go to seller):
// Create a PaymentIntent that routes funds to the connected account
export async function createDestinationCharge(params: {
amount: number; // in smallest currency unit
currency: string;
customerId?: string;
connectedAccountId: string;
appFee: number; // your fee in smallest currency unit
}) {
const intent = await stripe.paymentIntents.create({
amount: params.amount,
currency: params.currency,
payment_method_types: ['card'],
application_fee_amount: params.appFee,
transfer_data: { destination: params.connectedAccountId },
metadata: { orderId: 'ord_123' }
});
return intent.client_secret; // Confirm on client with Payment Element
}
Direct charges for Standard (seller owns the charge; you take a fee):
// Note: The charge is created on the connected account
export async function createDirectChargeOnSeller(params: {
amount: number;
currency: string;
connectedAccountId: string;
appFee: number;
}) {
const intent = await stripe.paymentIntents.create({
amount: params.amount,
currency: params.currency,
payment_method_types: ['card'],
application_fee_amount: params.appFee,
// No transfer_data here: charge is on the connected account
}, {
stripeAccount: params.connectedAccountId
});
return intent.client_secret;
}
Separate charges and transfers (multi-seller cart or delayed allocation):
// 1) Charge customer on the platform account
export async function createPlatformCharge(amount: number, currency: string) {
const intent = await stripe.paymentIntents.create({
amount, currency, payment_method_types: ['card'], metadata: { cartId: 'cart_789' }
});
return intent.client_secret;
}
// 2) After payment succeeds, split to sellers
export async function splitTransfers(orderId: string, splits: Array<{connectedAccountId: string; amount: number;}>) {
for (const s of splits) {
await stripe.transfers.create({
amount: s.amount,
currency: 'usd',
destination: s.connectedAccountId,
metadata: { orderId }
});
}
}
Webhook basics (dedupe and state updates):
import type { Request, Response } from 'express';
export function handleWebhook(req: Request, res: Response) {
const sig = req.headers['stripe-signature'] as string;
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return res.status(400).send(`Webhook Error: ${(err as Error).message}`);
}
// Idempotency: check event.id in Redis before processing
switch (event.type) {
case 'account.updated':
// Update seller KYC/capabilities in DB
break;
case 'payment_intent.succeeded':
// Mark order paid, enqueue transfers if using separate charges
break;
case 'payout.failed':
// Notify seller, flag retry
break;
case 'charge.dispute.created':
// Start dispute workflow
break;
}
res.json({ received: true });
}
Decision tree: picking your model
- Do your sellers want their own Stripe dashboards and already use Stripe? Choose Direct (Standard) with Direct charges.
- Do you need to onboard many sellers quickly with minimal build, while keeping platform-level control over fees and some payout rules? Choose Express with Destination charges.
- Do you need a fully white-labeled seller experience, advanced payout logic (holds, rolling reserves, multi-currency routing), or region-specific onboarding customizations? Choose Custom with Destination or Separate charges and transfers.
- Multi-seller carts or complex after-the-fact allocation? Favor Separate charges and transfers (Express or Custom).
- Minimal compliance burden, fastest time-to-market? Express.
Note: some marketplaces start on Express and migrate advanced cohorts to Custom later. Bake migration hooks into your data model (store account type per seller; isolate dashboard assumptions behind feature flags).
What breaks in production
- Verification dead-ends: Sellers abandon onboarding when asked for unexpected documents. Make
account.updateda first-class signal and re-prompt contextually in your UI. For Custom, progressively disclose requirements. - Negative balances: Separate charges and transfers can push a connected account negative (refunds/chargebacks). Implement a reserve policy, and block payouts when
balance < reserve_threshold. - Payout failures: Bank accounts change; names mismatch; countries have local rails quirks. Subscribe to
payout.failedandpayout.canceled, auto-retry with exponential backoff, and notify sellers with clear remediation steps. - Disputes timing: Dispute windows vary; do not auto-release all holds immediately. Keep evidence artifacts (order logs, shipment, comms) indexed by
charge.id. - Webhook duplication: Stripe retries on timeout. Use idempotency with
event.idstored in Redis; ensure handlers are re-entrant. Also verify signatures; don’t expose webhooks to the public without WAF/rate limits. - Rounding/currency gotchas: Always compute fees and splits in integer minor units. For multi-currency, decide on a settlement currency and pre-calc FX or use Stripe’s balance transactions to reconcile.
- Capability drift: Capabilities can be revoked (e.g., failed verification). Guard charge paths with live capability checks; auto-disable listings on failure.
- Tax forms and year-end reporting: If you operate in regions requiring platform-issued forms, plan a data pipeline months ahead. Even if Stripe handles filings in some regions, you must surface accurate summaries in your dashboard.
Building the seller experience (UX and state machine)
Even on Express, you need a robust state machine for the seller lifecycle:
- invited → onboarding_link_sent → onboarding_incomplete → verified → payouts_enabled → restricted (needs_info) → disabled.
- Model
capabilities.card_paymentsandcapabilities.transfersas feature gates. Don’t let a seller go live until both areactive. - Show proactive blockers with precise copy (e.g., “We need proof of address. Upload a recent utility bill.”) and deep-link to the Stripe-hosted update flow (Express) or your own upload flow (Custom).
Provide a minimum viable seller dashboard even with Express:
- Balances and next payout estimate (read from
/balanceand upcoming payouts via stripe API on the connected account with OAuth or server-side on behalf of account). - Dispute center: a simplified evidence upload UI that maps to the charge ID.
- Settings: Bank details update link (Express has an update link you can generate via
accountLinkstypeaccount_update).
Payments, methods, and regions
Cards are the default, but conversion goes up when you offer local rails. Design your abstraction so Stripe payment methods are just another capability flag in checkout. For Brazil-focused platforms, PIX often outperforms cards for certain verticals; we’ve compared trade-offs here: Brazil PIX vs Visa/Mastercard. When you activate methods, confirm your refund, dispute, and payout semantics per method—ACH and SEPA timings differ materially from cards.
Checklist:
- Enable Payment Element to unify card + wallets + local methods.
- Map each method to capture timing, dispute model, and settlement delays.
- For asynchronous methods (e.g., bank transfers), avoid marking orders as fulfilled until the
succeededevent, not onrequires_action.
Observability and reconciliation
Your finance team will thank you if you invest here first:
- Data warehouse mirror of balance transactions: Use Stripe’s reporting exports or API to ingest
balance_transactions,payouts, andchargesnightly. - Automated reconciliation: Match orders to balance transactions using
metadata.orderIdortransfer.groupfor batched splits. - Alerting: Payout failure rate, dispute rate by cohort, verification backlog age, funds held in reserve, event handler error rates.
- Audit log: For every fund move (charge, refund, transfer, payout), persist: who/what/when, Stripe object IDs, and pre/post balances if you track virtual balances.
Security and compliance posture
- PCI: Use Stripe-hosted UIs (Checkout/Payment Element) to reduce scope. Never log PANs.
- PII: Minimize storage. For Custom, prefer tokenized identity flows; if you must store documents temporarily, encrypt at rest and auto-expire.
- Key management: Rotate keys, segment by environment, forbid shell access to production secrets.
- Access: Follow the principle of least privilege when acting on behalf of connected accounts. For Standard, use OAuth with granular scopes.
- Edge: Protect webhooks and admin routes with a WAF and egress allowlists. Cloudflare, Fastly, or AWS WAF are typical choices. More patterns in building for the future with Cloudflare.
Cost and ROI: what it actually takes
Direct costs you’ll encounter:
- Per-transaction processing fees charged by Stripe (vary by method/region), plus any Connect platform fees.
- Per-connected-account and per-payout fees (model-dependent), plus identity verification fees if you use Stripe Identity.
- Dispute/chargeback fees.
Build and ops costs:
- Engineering: 2–6 weeks for a solid Express integration with multi-seller cart and payouts; 2–3 months for Custom with full KYC UI, dashboards, and reporting—assuming a small, senior team.
- Support: Plan for 1st-line support trained on common verification and payout issues. Budget extra staffing during initial seller onboarding waves.
- Finance/Compliance: Reconciliation jobs, year-end forms handling (region-dependent), policies for reserves/holds.
ROI levers:
- Faster onboarding increases GMV earlier; Express typically gets you there sooner.
- Take rate: Application fees are straightforward; value-added services (promotion, insurance, fulfillment) can increase take rate without hurting conversion.
- Dispute reduction: Proactive KYC + clear buyer comms lower dispute costs.
Hidden/expensive mistakes:
- Underbuilding reconciliation: You won’t notice until month-end when payouts don’t tie. Fixing it retroactively is painful.
- Picking the wrong model: Migrating from Standard to Express or Custom means re-consenting every seller.
- Ignoring negative balances: A single large refund can stall a seller’s payouts and tank trust if you don’t have reserves.
Testing and release strategy
- Local dev: Use Stripe CLI to forward webhooks to your machine, replay events, and seed accounts. Keep fixtures for
account.capabilitiestransitions. - Sandboxes: Create at least two test projects (integration and staging) to avoid polluted data sets.
- Load and chaos: Simulate webhook retries and out-of-order events. Ensure idempotent handlers survive.
- Data migrations: Roll out with feature flags per account type; maintain dual write paths if you’re switching charge flows.
Operational runbook must-haves:
- How to manually resend onboarding links.
- How to trigger a payout retry safely.
- How to freeze a seller (stop listing, stop transfers) without impacting past orders.
Example end-to-end flow (Express + Separate charges and transfers)
- Buyer creates a cart with 3 sellers → you collect payment once on the platform.
- On
payment_intent.succeeded, you compute splits and issue 3 transfers. - You record
transfer.group = orderIdto ease reconciliation. - If one seller is unverified or
transferscapability isinactive, you park funds in a platform-held reserve and retry once verification clears. - For refunds, you reverse the platform charge first, then create negative transfers or transfer reversals to pull funds back if needed.
When to revisit your decision
- You’re adding regulated categories or geographies with different KYC norms → Express to Custom.
- Sellers demand more control and reporting → Standard for those cohorts or build richer dashboards on Custom.
- Your cart complexity grows (bundles, subscriptions + usage fees) → move from Destination to Separate charges and transfers to avoid edge cases.
A brief note on subscriptions: Stripe Billing works with Connect, but the proration and invoicing life cycle across many sellers can get tricky. Keep subscriptions per seller where possible, or centralize billing on the platform and split via transfers.
FAQ
What’s the fastest way to launch a compliant marketplace on Stripe?
Express with Destination charges. You get Stripe-hosted onboarding, reasonable brand control, and manageable compliance overhead. Ship v1, then iterate.
Can I switch from Express to Custom later without re-onboarding sellers?
You can switch the account type only by creating a new connected account and migrating—sellers must re-consent to new agreements. Plan migrations with clear comms and incentives.
How do I handle a cart with multiple sellers in one payment?
Use Separate charges and transfers: charge the buyer on the platform, then issue transfers per seller. Track transfer.group and guard against negative balances for refunds/chargebacks.
Who handles taxes and year-end forms?
It depends on region and your platform role. Stripe offers tax reporting in some jurisdictions, but you still need to surface summaries and ensure data completeness. Consult counsel early.
What happens if a seller fails verification after making sales?
Funds are held; payouts are paused. Implement a reserve and notify the seller to complete verification. If they never complete, you may need to refund buyers or resolve per your terms.
Which payment methods should I enable first?
Start with cards + Wallets. Add local rails based on your markets (e.g., ACH, SEPA, PIX). Validate refund/dispute semantics before enabling at scale.
Key takeaways
- Choose Express for speed and balanced control; Custom for full white-label and complex payout logic; Direct (Standard) when sellers want their own Stripe.
- Align your charge model (Destination, Direct, Separate) with compliance, UX, and cart complexity from day one.
- Model seller lifecycle states and capabilities; treat
account.updatedas a core event in your app. - Build reconciliation, reserves, and idempotent webhooks before you scale GMV—retrofits are expensive.
- Expect and design for failure: payout errors, disputes, verification loops, and out-of-order webhooks.
- Keep your payment method strategy modular; add local rails only when you can support their operational semantics.
If you’re building a marketplace and want a production-ready Stripe Connect architecture without the potholes, MTBYTE can help design, implement, and ship it. Tell us about your scope at /contact.
NEXT STEP
Liked the approach?
We apply the same principles to client projects: AI, automation, products that don't die after launch.