/stripe-guides

How to use Stripe Connect for marketplace payments?

Learn how to set up and use Stripe Connect for marketplace payments, from account creation to payment flows, webhooks, payouts, and security best practices.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to use Stripe Connect for marketplace payments?

How to Use Stripe Connect for Marketplace Payments: A Comprehensive Guide

 

Introduction

 

Stripe Connect is a powerful platform that enables marketplace businesses to facilitate payments between customers and service providers or sellers. This guide will walk you through setting up and implementing Stripe Connect for your marketplace application, covering everything from account setup to handling complex payment flows.

 

Step 1: Understanding Stripe Connect Models

 

Before implementation, understand the three Stripe Connect account types:

  • Standard accounts: Users create and manage their own Stripe accounts. Minimal setup but users must complete Stripe's onboarding.
  • Express accounts: Streamlined onboarding with Stripe-hosted pages and your branding. Good balance of simplicity and customization.
  • Custom accounts: Fully white-labeled experience where you collect all seller information. Most control but highest compliance requirements.

For most marketplaces, Express accounts offer the best balance of user experience and development complexity.

 

Step 2: Setting Up Your Stripe Account

 

  1. Create a Stripe account at https://dashboard.stripe.com/register
  2. Complete verification requirements
  3. Navigate to Settings → Connect settings
  4. Configure your platform settings:
  • Set your platform display name
  • Upload your logo
  • Configure redirect URLs for OAuth
  • Set up webhook endpoints

 

Step 3: Installing Stripe Libraries

 

Install the Stripe server-side library for your programming language:

For Node.js:


npm install stripe

For Python:


pip install stripe

For PHP:


composer require stripe/stripe-php

For Ruby:


gem install stripe

 

Step 4: Initialize Stripe in Your Application

 

Node.js:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

Python:


import stripe
stripe.api_key = "sk_test_YOUR_SECRET\_KEY"

PHP:


\Stripe\Stripe::setApiKey('sk_test_YOUR_SECRET_KEY');

Ruby:


require 'stripe'
Stripe.api_key = 'sk_test_YOUR_SECRET\_KEY'

 

Step 5: Creating Connected Accounts (Express)

 

For Express accounts, you'll create an account and then redirect users to the Stripe-hosted onboarding:

Node.js:


app.post('/create-express-account', async (req, res) => {
  try {
    // Create a new Express account
    const account = await stripe.accounts.create({
      type: 'express',
      capabilities: {
        card\_payments: {requested: true},
        transfers: {requested: true},
      },
      business\_type: 'individual',
      business\_profile: {
        mcc: '5734', // Computer Software Stores
        url: 'https://example.com',
      },
      metadata: {
        user\_id: req.body.userId // Store your internal user ID
      }
    });
    
    // Save the account ID to your database
    // yourDatabase.saveStripeAccountId(req.body.userId, account.id);
    
    // Generate an account link for onboarding
    const accountLink = await stripe.accountLinks.create({
      account: account.id,
      refresh\_url: 'https://example.com/reauth',
      return\_url: 'https://example.com/success',
      type: 'account\_onboarding',
    });
    
    // Redirect to the Stripe hosted onboarding
    res.redirect(accountLink.url);
  } catch (error) {
    console.error('Error creating Express account:', error);
    res.status(500).send({ error: error.message });
  }
});

 

Step 6: Setting Up Webhooks

 

Webhooks are essential for receiving updates about connected accounts:

  1. Set up your webhook endpoint:

app.post('/stripe-webhook', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      'whsec_YOUR_WEBHOOK\_SECRET'
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle specific events
  switch (event.type) {
    case 'account.updated':
      const account = event.data.object;
      console.log('Account updated:', account.id);
      // Update your database with account details
      break;
    case 'account.application.deauthorized':
      // The user has disconnected their account
      break;
    // Handle other events...
  }

  res.json({received: true});
});
  1. Register your webhook URL in the Stripe dashboard:

 

Step 7: Processing Direct Charges

 

For marketplaces where customers pay your platform and you pay sellers:


app.post('/create-payment', async (req, res) => {
  try {
    const { amount, currency, customer, description, connected_account_id, application_fee_amount } = req.body;
    
    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency,
      customer,
      description,
      application_fee_amount, // Your platform fee
      transfer\_data: {
        destination: connected_account_id, // The seller's account ID
      },
    });
    
    res.send({
      clientSecret: paymentIntent.client\_secret,
    });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Step 8: Implementing Payment Intent on the Client Side

 

Add Stripe.js to your client:



Use Elements to collect payment information:


// Initialize Stripe
const stripe = Stripe('pk_test_YOUR_PUBLISHABLE_KEY');
const elements = stripe.elements();

// Create card element
const cardElement = elements.create('card');
cardElement.mount('#card-element');

// Handle form submission
document.getElementById('payment-form').addEventListener('submit', async (event) => {
  event.preventDefault();
  
  const { error, paymentIntent } = await stripe.confirmCardPayment(
    clientSecret, // Received from server
    {
      payment\_method: {
        card: cardElement,
        billing\_details: {
          name: document.getElementById('name').value,
        },
      },
    }
  );
  
  if (error) {
    // Show error to customer
    const errorElement = document.getElementById('card-errors');
    errorElement.textContent = error.message;
  } else if (paymentIntent.status === 'succeeded') {
    // Payment successful
    window.location.href = '/payment-success';
  }
});

 

Step 9: Implementing Transfers for Existing Payments

 

If you need to transfer funds after a payment has been processed:


app.post('/create-transfer', async (req, res) => {
  try {
    const { amount, destination, source\_transaction } = req.body;
    
    const transfer = await stripe.transfers.create({
      amount,
      currency: 'usd',
      destination, // Connected account ID
      source\_transaction, // Payment Intent ID if transferring from a specific payment
      transfer_group: req.body.order_id, // Optional grouping identifier
    });
    
    res.send({ success: true, transfer });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Step 10: Implementing Destination Charges

 

For marketplaces where charges occur directly on the connected account:


// First, create a payment method on your platform
app.post('/create-payment-method', async (req, res) => {
  try {
    const paymentMethod = await stripe.paymentMethods.create({
      type: 'card',
      card: {
        number: '4242424242424242',
        exp\_month: 12,
        exp\_year: 2023,
        cvc: '123',
      },
      billing\_details: {
        name: 'Customer Name',
      },
    });
    
    res.send({ paymentMethod });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

// Then create a charge on the connected account
app.post('/create-destination-charge', async (req, res) => {
  try {
    const { amount, connected_account_id, payment_method_id } = req.body;
    
    // Create a PaymentIntent on the connected account
    const paymentIntent = await stripe.paymentIntents.create(
      {
        amount,
        currency: 'usd',
        payment_method: payment_method\_id,
        confirmation\_method: 'manual',
        confirm: true,
        application_fee_amount: Math.round(amount \* 0.1), // 10% platform fee
      },
      {
        stripeAccount: connected_account_id, // Specify the connected account
      }
    );
    
    res.send({ success: true, paymentIntent });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Step 11: Managing Payouts

 

Control when funds are paid out to your connected accounts:


// Update payout schedule for a connected account
app.post('/update-payout-schedule', async (req, res) => {
  try {
    const { connected_account_id, interval, monthly_anchor, weekly_anchor } = req.body;
    
    const account = await stripe.accounts.update(
      connected_account_id,
      {
        settings: {
          payouts: {
            schedule: {
              interval: interval, // 'manual', 'daily', 'weekly', 'monthly'
              monthly_anchor: monthly_anchor, // Required if interval is 'monthly'
              weekly_anchor: weekly_anchor, // Required if interval is 'weekly'
            },
          },
        },
      }
    );
    
    res.send({ success: true, account });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

// Create an instant payout for a connected account
app.post('/create-payout', async (req, res) => {
  try {
    const { connected_account_id, amount } = req.body;
    
    const payout = await stripe.payouts.create(
      {
        amount,
        currency: 'usd',
      },
      {
        stripeAccount: connected_account_id,
      }
    );
    
    res.send({ success: true, payout });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Step 12: Handling Disputes and Refunds

 

Implement refund functionality:


app.post('/refund-payment', async (req, res) => {
  try {
    const { payment_intent_id, amount } = req.body;
    
    const refund = await stripe.refunds.create({
      payment_intent: payment_intent\_id,
      amount: amount, // Optional: partial refund amount
    });
    
    res.send({ success: true, refund });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

Set up dispute handling webhook:


// In your webhook handler
case 'charge.dispute.created':
  const dispute = event.data.object;
  // Notify the seller about the dispute
  // Store dispute information in your database
  await notifySeller(dispute.payment\_intent, 'A dispute has been filed against your charge');
  break;

 

Step 13: Managing Connected Accounts

 

Retrieve account details:


app.get('/account/:id', async (req, res) => {
  try {
    const account = await stripe.accounts.retrieve(req.params.id);
    res.send({ account });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

Generate a new account link for incomplete onboarding:


app.post('/account-links', async (req, res) => {
  try {
    const { account\_id } = req.body;
    
    const accountLink = await stripe.accountLinks.create({
      account: account\_id,
      refresh\_url: 'https://example.com/reauth',
      return\_url: 'https://example.com/return',
      type: 'account\_onboarding',
    });
    
    res.send({ url: accountLink.url });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Step 14: Implementing Split Payments

 

For complex scenarios where funds need to be split among multiple recipients:


app.post('/create-split-payment', async (req, res) => {
  try {
    const { amount, customer, description, recipients } = req.body;
    
    // First create a payment intent on the platform
    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency: 'usd',
      customer,
      description,
      payment_method_types: ['card'],
      capture\_method: 'manual', // Authorize now, capture later
    });
    
    // Store payment intent and recipient information for later distribution
    // yourDatabase.storeSplitPaymentInfo(paymentIntent.id, recipients);
    
    res.send({
      clientSecret: paymentIntent.client\_secret
    });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

// After payment is authorized, capture and distribute funds
app.post('/capture-and-distribute', async (req, res) => {
  try {
    const { payment_intent_id } = req.body;
    
    // Capture the payment
    const paymentIntent = await stripe.paymentIntents.capture(payment_intent_id);
    
    // Get the recipients data from your database
    // const recipients = await yourDatabase.getSplitPaymentRecipients(payment_intent_id);
    const recipients = req.body.recipients; // For demo purposes
    
    // Create transfers to each recipient
    const transfers = [];
    for (const recipient of recipients) {
      const transfer = await stripe.transfers.create({
        amount: recipient.amount,
        currency: 'usd',
        destination: recipient.account\_id,
        source\_transaction: paymentIntent.charges.data[0].id,
        description: `Payment for order ${req.body.order_id}`,
      });
      transfers.push(transfer);
    }
    
    res.send({ success: true, transfers });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Step 15: Reporting and Analytics

 

Implement reporting to track marketplace performance:


app.get('/platform-balance', async (req, res) => {
  try {
    const balance = await stripe.balance.retrieve();
    res.send({ balance });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

app.get('/connected-account-balance/:id', async (req, res) => {
  try {
    const balance = await stripe.balance.retrieve({
      stripeAccount: req.params.id,
    });
    res.send({ balance });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

app.get('/transaction-history', async (req, res) => {
  try {
    const { limit, starting\_after } = req.query;
    
    const balanceTransactions = await stripe.balanceTransactions.list({
      limit: limit || 10,
      starting_after: starting_after || undefined,
    });
    
    res.send({ transactions: balanceTransactions });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Step 16: Security and Compliance

 

Implement best practices for security:

  1. Keep your Stripe API keys secure:
  • Never expose secret keys in client-side code
  • Use environment variables for storing keys

// Load environment variables
require('dotenv').config();

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
  1. Validate webhook signatures:

app.post('/stripe-webhook', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  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.message}`);
  }

  // Process event...
  res.json({received: true});
});
  1. Implement proper error handling and logging for all Stripe API calls

 

Step 17: Testing Your Integration

 

  1. Use Stripe's test mode:
  • Test API keys start with sk_test_ and pk_test_
  • Test card numbers like 4242 4242 4242 4242
  1. Test the full payment flow:

// Test script for creating and paying out to a connected account
async function testConnectFlow() {
  try {
    // 1. Create a connected account
    const account = await stripe.accounts.create({
      type: 'express',
      capabilities: {
        card\_payments: {requested: true},
        transfers: {requested: true},
      },
      business\_type: 'individual',
    });
    console.log('Created account:', account.id);
    
    // 2. Create a payment on the platform
    const paymentIntent = await stripe.paymentIntents.create({
      amount: 1000, // $10.00
      currency: 'usd',
      payment_method: 'pm_card\_visa', // Test payment method
      confirm: true,
    });
    console.log('Created payment:', paymentIntent.id);
    
    // 3. Transfer funds to the connected account
    const transfer = await stripe.transfers.create({
      amount: 800, // $8.00 (keeping $2 as platform fee)
      currency: 'usd',
      destination: account.id,
      source\_transaction: paymentIntent.charges.data[0].id,
    });
    console.log('Created transfer:', transfer.id);
    
    return { success: true, account, paymentIntent, transfer };
  } catch (error) {
    console.error('Test failed:', error);
    return { success: false, error };
  }
}
  1. Use Stripe's CLI for local webhook testing:
  • Install the Stripe CLI
  • Run stripe listen --forward-to localhost:3000/stripe-webhook

 

Step 18: Going Live

 

Before switching to production:

  1. Complete Stripe's Go Live checklist
  2. Switch to live API keys (starting with sk_live_ and pk_live_)
  3. Update webhook endpoints to production URLs
  4. Test the full flow in production with real cards (make small test transactions)
  5. Set up monitoring and alerts for critical events

 

Step 19: Advanced Features

 

Implement additional marketplace features:

  1. Subscription billing for sellers:

app.post('/create-seller-subscription', async (req, res) => {
  try {
    const { connected_account_id, price\_id } = req.body;
    
    // Create a Customer object for the connected account
    const customer = await stripe.customers.create({
      email: req.body.email,
      metadata: {
        connected_account_id,
      },
    });
    
    // Attach a payment method
    await stripe.paymentMethods.attach(req.body.payment_method_id, {
      customer: customer.id,
    });
    
    // Set as default payment method
    await stripe.customers.update(customer.id, {
      invoice\_settings: {
        default_payment_method: req.body.payment_method_id,
      },
    });
    
    // Create subscription
    const subscription = await stripe.subscriptions.create({
      customer: customer.id,
      items: [{ price: price\_id }],
      expand: ['latest_invoice.payment_intent'],
    });
    
    res.send({ subscription });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});
  1. Escrow payments with delayed payouts:

app.post('/create-escrow-payment', async (req, res) => {
  try {
    const { amount, currency, customer, connected_account_id } = req.body;
    
    // 1. Create a payment intent
    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency,
      customer,
      payment_method_types: ['card'],
      capture\_method: 'manual', // Authorize only, capture later
    });
    
    // 2. Store the payment intent and connected account in your database
    // await yourDatabase.storeEscrowPayment({
    //   payment_intent_id: paymentIntent.id,
    //   connected_account_id,
    //   amount,
    //   status: 'pending'
    // });
    
    res.send({
      clientSecret: paymentIntent.client\_secret,
    });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

// Later, when service is completed:
app.post('/release-escrow', async (req, res) => {
  try {
    const { payment_intent_id, connected_account_id } = req.body;
    
    // 1. Capture the payment
    const paymentIntent = await stripe.paymentIntents.capture(payment_intent_id);
    
    // 2. Create a transfer to the connected account
    const platformFee = Math.round(paymentIntent.amount \* 0.1); // 10% fee
    const transferAmount = paymentIntent.amount - platformFee;
    
    const transfer = await stripe.transfers.create({
      amount: transferAmount,
      currency: paymentIntent.currency,
      destination: connected_account_id,
      source\_transaction: paymentIntent.charges.data[0].id,
    });
    
    // 3. Update your database
    // await yourDatabase.updateEscrowPayment(payment_intent_id, 'completed');
    
    res.send({ success: true, transfer });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Step 20: Scaling Your Marketplace

 

As your marketplace grows:

  1. Implement idempotency keys for all API requests to prevent duplicate operations:

app.post('/create-payment', async (req, res) => {
  try {
    const idempotencyKey = req.body.order\_id; // Use a unique identifier
    
    const paymentIntent = await stripe.paymentIntents.create(
      {
        amount: req.body.amount,
        currency: req.body.currency,
        customer: req.body.customer,
        // other parameters...
      },
      {
        idempotencyKey: idempotencyKey,
      }
    );
    
    res.send({ clientSecret: paymentIntent.client\_secret });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});
  1. Implement robust error handling and retry logic:

async function retryStripeOperation(operation, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      
      // Only retry certain types of errors
      if (error.type === 'StripeConnectionError' || error.type === 'StripeAPIError') {
        // Exponential backoff
        const delay = Math.pow(2, attempt) \* 100;
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      
      // For other types of errors, don't retry
      throw error;
    }
  }
  
  // If we've exhausted all retries
  throw lastError;
}

// Usage
app.post('/create-transfer', async (req, res) => {
  try {
    const transfer = await retryStripeOperation(() => 
      stripe.transfers.create({
        amount: req.body.amount,
        currency: 'usd',
        destination: req.body.connected_account_id,
      })
    );
    
    res.send({ success: true, transfer });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

 

Conclusion

 

Implementing Stripe Connect for your marketplace involves many components, from account creation to payment processing and funds distribution. This guide covered the essential steps to build a robust marketplace payment system with Stripe Connect. Remember to thoroughly test your implementation and follow Stripe's best practices for security and compliance.

As your marketplace grows, continuously monitor and optimize your payment flows to provide the best experience for both your customers and sellers while minimizing costs and risks.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022