Guides

Building Marketplaces with open-source tools

This guide outlines the technical implementation of a multi-party payment system for a two-sided marketplace using Stripe Connect. We focus on the 'Destination Charges' model, which allows a platform to facilitate transactions between buyers and sellers while automatically deducting a commission fee before funds reach the seller's account.

4-6 hours5 steps
1

Extend User Schema for Connect Account Mapping

To facilitate payments, you must map your application's 'Seller' entity to a Stripe Connect Account ID. Update your database schema to store the Stripe Account ID and a status flag indicating if the seller has completed the onboarding process.

migrations/001_add_stripe_fields.sql
ALTER TABLE users 
ADD COLUMN stripe_connect_id TEXT UNIQUE,
ADD COLUMN stripe_onboarding_complete BOOLEAN DEFAULT FALSE;

⚠ Common Pitfalls

  • Storing the Stripe Secret Key in the database instead of environment variables.
  • Failing to index the stripe_connect_id column, leading to slow lookups during webhook processing.
2

Implement the Onboarding Link Flow

Create an Express or Next.js API route that generates a Stripe Account Link. This link redirects the seller to Stripe's hosted onboarding UI. Upon completion, Stripe redirects the user back to your platform.

api/stripe/onboard.js
const accountLink = await stripe.accountLinks.create({
  account: sellerStripeId,
  refresh_url: 'https://yourmarket.com/reauth',
  return_url: 'https://yourmarket.com/dashboard',
  type: 'account_onboarding',
});
return res.json({ url: accountLink.url });

⚠ Common Pitfalls

  • Assuming a single redirect means onboarding is finished; you must verify the 'details_submitted' state via webhooks or account retrieval.
  • Hardcoding return URLs instead of using environment-specific base URLs.
3

Create Checkout Sessions with Application Fees

When a buyer initiates a purchase, create a Stripe Checkout Session. Use the 'transfer_data' parameter to specify the seller's account and the 'application_fee_amount' to define your platform's commission in cents.

api/stripe/create-checkout.js
const session = await stripe.checkout.sessions.create({
  mode: 'payment',
  line_items: [{ price: 'price_id', quantity: 1 }],
  payment_intent_data: {
    application_fee_amount: 500, // $5.00 fee
    transfer_data: {
      destination: sellerStripeId,
    },
  },
  success_url: 'https://yourmarket.com/success',
  cancel_url: 'https://yourmarket.com/cancel',
});

⚠ Common Pitfalls

  • Calculating fees on the client-side, which allows users to manipulate the commission amount.
  • Neglecting to handle currency precision; Stripe requires amounts in the smallest currency unit (e.g., cents).
4

Handle Webhook Events for State Reconciliation

Listen for 'checkout.session.completed' and 'account.updated' events. Use these to update your internal database records for order fulfillment and seller verification status.

api/webhooks/stripe.js
const event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);

if (event.type === 'checkout.session.completed') {
  const session = event.data.object;
  await db.orders.update({ where: { sessionId: session.id }, data: { status: 'PAID' } });
}

if (event.type === 'account.updated') {
  const account = event.data.object;
  if (account.details_submitted) {
    await db.users.update({ where: { stripe_connect_id: account.id }, data: { stripe_onboarding_complete: true } });
  }
}

⚠ Common Pitfalls

  • Processing webhooks without verifying the 'stripe-signature' header, exposing your DB to spoofed requests.
  • Not returning a 200 status code immediately, causing Stripe to retry and potentially double-process events.
5

Implement Refund Logic with Fee Reversal

When a buyer requests a refund, you must decide if the platform fee is also refunded. Use the 'reverse_transfer' and 'refund_application_fee' flags to manage the flow of funds back from the seller's Connect account.

api/stripe/refund.js
const refund = await stripe.refunds.create({
  payment_intent: paymentIntentId,
  reverse_transfer: true,
  refund_application_fee: true,
});

⚠ Common Pitfalls

  • Attempting to refund more than the seller's current Stripe balance, which results in a failed refund and requires manual intervention.
  • Forgetting to reverse the transfer, which leaves the seller with the money while the platform absorbs the refund cost.

What you built

By implementing this flow, you have established a scalable payment architecture that handles automated splitting and seller onboarding. Ensure you monitor your Stripe Dashboard for 'Requirements Due' flags on Connect accounts to prevent payout blocks as regulations evolve.