Stripe Connect Split Payments with NestJS: A Real Marketplace Guide
Back to Blog
StripePaymentsNestJSBackendMarketplacestripe splitstripe connect split paymentssplit payments stripestripe split pay

Stripe Connect Split Payments with NestJS: A Real Marketplace Guide

July 1, 20265 min read45 views0 comments

How I built automatic payment splits between customers, drivers, and the platform in JobsiteX using Stripe Connect, NestJS destination charges, and webhooks. With the code and the mistakes to avoid.

When I built JobsiteX, a marketplace app that connects customers with delivery drivers, the hardest requirement was not the app itself. It was the money flow. A customer pays once, but that single payment has to split three ways: the driver gets their share, the platform keeps its commission, and everything must be trackable and refundable. This is exactly the problem Stripe Connect solves, and in this post I will walk through how I implemented it with a NestJS backend.

Why Stripe Connect and not plain Stripe?

A normal Stripe integration has two parties: your business and the customer. A marketplace has three or more. If you try to fake it with a single Stripe account, you end up holding other people's money in your own balance, manually paying them out, and taking on legal and tax responsibility that you really do not want.

Stripe Connect gives every driver (or seller, or vendor) their own connected account. Stripe handles onboarding, identity verification (KYC), and payouts to their bank. Your platform just decides how each payment is split.

There are three account types. I chose Express accounts because they give the best balance: Stripe hosts the onboarding and dashboard, but the platform controls the payment flow.

Step 1: Onboarding drivers with Express accounts

When a driver signs up in JobsiteX, the backend creates a connected account and generates a one-time onboarding link:

// drivers.service.ts (NestJS)
const account = await this.stripe.accounts.create({
  type: 'express',
  country: 'US',
  email: driver.email,
  capabilities: {
    transfers: { requested: true },
  },
});

const link = await this.stripe.accountLinks.create({
  account: account.id,
  refresh_url: `${APP_URL}/onboarding/retry`,
  return_url: `${APP_URL}/onboarding/done`,
  type: 'account_onboarding',
});

I store account.id on the driver's row in Postgres. The driver opens the link, fills in their details on Stripe's hosted page, and Stripe tells me when they are ready through the account.updated webhook. Until charges_enabled and payouts_enabled are both true, the driver cannot accept jobs.

Step 2: Splitting the payment

For each completed job, the customer pays the full amount and Stripe routes the driver's share automatically. The cleanest way is destination charges with an application_fee_amount:

const paymentIntent = await this.stripe.paymentIntents.create({
  amount: 5000,               // $50.00 total, in cents
  currency: 'usd',
  application_fee_amount: 750, // $7.50 platform commission
  transfer_data: {
    destination: driver.stripeAccountId,
  },
});

That is the entire split. The customer pays $50, the platform keeps $7.50, and $42.50 lands in the driver's connected account. No cron jobs, no manual transfers, no spreadsheet.

One thing that bit me early: always calculate the fee in cents on the server. I originally computed the commission on the client and sent it with the request. A curious user with dev tools open could have set their own commission. Everything money-related must live server side.

Step 3: Webhooks are the source of truth

A payment is not done when the API call returns. Cards get declined, banks reverse transactions, 3D Secure challenges expire. The only reliable record is the webhook stream, so my NestJS webhook controller is small but strict:

@Post('webhook')
async handleWebhook(@Req() req: RawBodyRequest<Request>) {
  const event = this.stripe.webhooks.constructEvent(
    req.rawBody,
    req.headers['stripe-signature'],
    process.env.STRIPE_WEBHOOK_SECRET,
  );

  switch (event.type) {
    case 'payment_intent.succeeded':
      await this.jobs.markPaid(event.data.object.metadata.jobId);
      break;
    case 'payment_intent.payment_failed':
      await this.jobs.markFailed(event.data.object.metadata.jobId);
      break;
    case 'account.updated':
      await this.drivers.syncAccountStatus(event.data.object);
      break;
  }
  return { received: true };
}

Two practical notes. First, NestJS parses JSON bodies by default, which breaks Stripe's signature check; you need the raw body (rawBody: true in NestFactory.create). Second, make every handler idempotent, because Stripe retries webhooks. Marking an already-paid job as paid again must be a no-op.

Step 4: Withdrawals and payout timing

Express accounts get automatic payouts on a rolling schedule by default, and for most drivers that is what they want. JobsiteX also offers an "instant withdraw" button that triggers an instant payout for a small fee where the driver's bank supports it:

await this.stripe.payouts.create(
  { amount: balance.available, currency: 'usd', method: 'instant' },
  { stripeAccount: driver.stripeAccountId },
);

What I would tell past me

  • Start in test mode with Stripe's test connected accounts; you can simulate the entire onboarding and payout cycle without real money.
  • Store Stripe IDs (customer, account, payment intent) on your own tables from day one. Debugging without them is painful.
  • Put every amount in integer cents everywhere. Floating point money bugs are real and they are embarrassing.
  • Read the fee documentation carefully: Stripe's processing fee comes out of the platform's side with destination charges, so price your commission with that in mind.

Stripe Connect took me about a week to integrate properly, and it removed an entire category of work: no payout scripts, no compliance headaches, no holding funds. If you are building any kind of marketplace with NestJS, it is the approach I would pick again.

See more of my work

Have a project that needs payments done right? Contact me and let's talk about it.

Tagged with

StripePaymentsNestJSBackendMarketplacestripe splitstripe connect split paymentssplit payments stripestripe split pay

Comments

Leave a comment

You must be signed in to comment.