# How to split payments between two users in Stripe

- Tool: Stripe
- Difficulty: Advanced
- Time required: 20 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+
- Last updated: March 2026

## TL;DR

Stripe Connect provides multiple approaches for splitting payments between parties: destination charges with application_fee_amount for simple two-way splits, and separate charges-and-transfers for complex multi-party splits. This guide covers both methods with working code, plus how to handle refunds, variable fees, and real-time split calculations.

## Splitting Payments Between Multiple Parties with Stripe

Payment splitting is a core requirement for marketplaces, platforms, and SaaS products that facilitate transactions between buyers and sellers. Stripe Connect offers two main approaches: destination charges (simple, built-in splitting) and separate charges-and-transfers (flexible, multi-party). Your choice depends on the number of recipients and the complexity of your fee structure.

## Before you start

- Stripe account with Connect enabled
- At least one connected account created
- Node.js 18 or later installed
- Stripe Node.js SDK: npm install stripe
- Express.js: npm install express

## Step-by-step guide

### 1. Simple two-way split with destination charges

The easiest way to split a payment between your platform and one seller. Use application_fee_amount to set your platform's cut. The rest goes to the seller.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

async function twoWaySplit() {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 10000, // $100.00 total
    currency: 'usd',
    payment_method_types: ['card'],
    application_fee_amount: 2000, // $20.00 platform fee
    transfer_data: {
      destination: 'acct_sellerAccount',
    },
  });
  // Seller receives $80.00, platform receives $20.00
  console.log('PaymentIntent:', paymentIntent.id);
  console.log('Client secret:', paymentIntent.client_secret);
  return paymentIntent;
}

twoWaySplit();
```

**Expected result:** A PaymentIntent is created. When confirmed, $80.00 goes to the seller and $20.00 to your platform.

### 2. Variable percentage-based fees

Calculate the platform fee as a percentage of the payment amount. This is common for marketplace models with tiered pricing.

```
function calculatePlatformFee(amountInCents, feePercentage) {
  return Math.round(amountInCents * (feePercentage / 100));
}

async function chargeWithPercentageFee(amount, sellerAccountId, feePercent) {
  const fee = calculatePlatformFee(amount, feePercent);

  const paymentIntent = await stripe.paymentIntents.create({
    amount: amount,
    currency: 'usd',
    payment_method_types: ['card'],
    application_fee_amount: fee,
    transfer_data: {
      destination: sellerAccountId,
    },
    metadata: {
      fee_percentage: feePercent.toString(),
      fee_amount: fee.toString(),
    },
  });

  console.log(`Total: $${(amount/100).toFixed(2)}`);
  console.log(`Fee (${feePercent}%): $${(fee/100).toFixed(2)}`);
  console.log(`Seller receives: $${((amount-fee)/100).toFixed(2)}`);
  return paymentIntent;
}

chargeWithPercentageFee(15000, 'acct_seller123', 15); // 15% fee on $150
```

**Expected result:** A $150 charge with a 15% ($22.50) platform fee. Seller receives $127.50.

### 3. Multi-party split with separate charges and transfers

For orders involving multiple sellers, charge the buyer once and create separate transfers to each seller.

```
async function multiPartySplit(orderItems) {
  // orderItems: [{ sellerAccountId, amount }, ...]
  const totalAmount = orderItems.reduce((sum, item) => sum + item.amount, 0);
  const platformFee = calculatePlatformFee(totalAmount, 10); // 10% platform fee

  // Step 1: Charge the buyer
  const paymentIntent = await stripe.paymentIntents.create({
    amount: totalAmount + platformFee,
    currency: 'usd',
    payment_method: 'pm_card_visa', // Use test card
    confirm: true,
    automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
  });

  // Step 2: Transfer to each seller
  const chargeId = paymentIntent.latest_charge;
  const transfers = [];

  for (const item of orderItems) {
    const transfer = await stripe.transfers.create({
      amount: item.amount,
      currency: 'usd',
      destination: item.sellerAccountId,
      source_transaction: chargeId,
      metadata: { order_id: paymentIntent.id },
    });
    transfers.push(transfer);
  }

  console.log(`Charged: $${((totalAmount + platformFee)/100).toFixed(2)}`);
  console.log(`Platform keeps: $${(platformFee/100).toFixed(2)}`);
  transfers.forEach(t => {
    console.log(`Transfer ${t.id}: $${(t.amount/100).toFixed(2)} → ${t.destination}`);
  });

  return { paymentIntent, transfers };
}

multiPartySplit([
  { sellerAccountId: 'acct_sellerA', amount: 5000 },
  { sellerAccountId: 'acct_sellerB', amount: 8000 },
]);
```

**Expected result:** One charge to the buyer, two transfers to sellers, and the platform retains its 10% fee.

### 4. Handle refunds on split payments

When refunding a destination charge, Stripe automatically reverses the transfer. For separate charges-and-transfers, you handle reversals manually.

```
// Refund a destination charge (automatic transfer reversal)
async function refundDestinationCharge(paymentIntentId, refundAmount) {
  const refund = await stripe.refunds.create({
    payment_intent: paymentIntentId,
    amount: refundAmount, // Partial refund in cents
    reverse_transfer: true, // Reverse the seller's transfer
    refund_application_fee: true, // Also refund the platform fee
  });
  console.log('Refund created:', refund.id);
  return refund;
}

// Refund with manual transfer reversals
async function refundWithReversals(paymentIntentId, transferIds) {
  const refund = await stripe.refunds.create({
    payment_intent: paymentIntentId,
  });

  for (const transferId of transferIds) {
    await stripe.transfers.createReversal(transferId);
  }

  console.log('Refund and reversals complete');
  return refund;
}
```

**Expected result:** The charge is refunded and the transfers to connected accounts are reversed.

## Complete code example

File: `split-payments.js`

```javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const app = express();

app.use(express.json());

function calculateFee(amount, percent) {
  return Math.round(amount * (percent / 100));
}

// Destination charge (two-way split)
app.post('/api/pay/single-seller', async (req, res) => {
  try {
    const { amount, sellerAccountId, feePercent } = req.body;
    const fee = calculateFee(amount, feePercent || 10);
    const pi = await stripe.paymentIntents.create({
      amount,
      currency: 'usd',
      payment_method_types: ['card'],
      application_fee_amount: fee,
      transfer_data: { destination: sellerAccountId },
      metadata: { fee_percent: String(feePercent || 10) },
    });
    res.json({ clientSecret: pi.client_secret, fee });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Separate charges-and-transfers (multi-seller split)
app.post('/api/pay/multi-seller', async (req, res) => {
  try {
    const { items, feePercent } = req.body;
    const sellerTotal = items.reduce((s, i) => s + i.amount, 0);
    const fee = calculateFee(sellerTotal, feePercent || 10);
    const total = sellerTotal + fee;

    const pi = await stripe.paymentIntents.create({
      amount: total,
      currency: 'usd',
      payment_method_types: ['card'],
    });

    res.json({
      clientSecret: pi.client_secret,
      paymentIntentId: pi.id,
      breakdown: { total, sellerTotal, platformFee: fee },
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Execute transfers after payment succeeds
app.post('/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}`);
  }

  if (event.type === 'payment_intent.succeeded') {
    const pi = event.data.object;
    // Look up order items from your database and create transfers
    console.log('Payment succeeded:', pi.id);
  }

  res.json({ received: true });
});

app.listen(3000, () => console.log('Split payments server on port 3000'));
```

## Common mistakes

- **Using floating-point math for fee calculations** — undefined Fix: Always use integer math in cents and Math.round() to avoid precision errors. 10000 * 0.15 = 1500 cents, not $15.00.
- **Setting application_fee_amount higher than the payment amount** — undefined Fix: The fee cannot exceed the total charge amount. Validate your fee calculation before creating the PaymentIntent.
- **Not handling refunds correctly for split payments** — undefined Fix: For destination charges, use reverse_transfer: true. For separate charges-and-transfers, manually reverse each transfer.
- **Creating transfers before the payment is confirmed** — undefined Fix: Only create transfers after payment_intent.succeeded webhook fires, or use source_transaction to tie transfers to the charge.

## Best practices

- Use destination charges for simple two-party splits and separate charges-and-transfers for multi-party splits
- Always calculate fees in cents using integer math with Math.round()
- Store the fee percentage and calculated amount in PaymentIntent metadata for auditing
- Use idempotency keys on transfers to prevent double-splits on retries
- Create transfers in the payment_intent.succeeded webhook handler for reliability
- Build a reconciliation system that matches charges to transfers
- Log all split calculations for financial reporting and dispute resolution

## Frequently asked questions

### Can I split a payment between more than two parties?

Yes. Use separate charges-and-transfers. Charge the buyer once and create individual transfers to each party. The total transfers cannot exceed the charge amount.

### Who pays the Stripe processing fees on a split payment?

On destination charges, processing fees are deducted from the platform's application_fee_amount. On separate charges, fees come from the platform's cut. You can structure your pricing to pass fees to sellers by adjusting the split.

### Can I change the split ratio after the payment is processed?

For destination charges, the split is fixed at payment time. For separate charges-and-transfers, you control when and how much you transfer, so you can adjust the split as long as you have not already transferred the full amount.

### What happens if a seller's account is not verified when I try to split a payment?

For destination charges, the payment fails if the connected account cannot accept charges. For separate charges-and-transfers, the charge succeeds on your platform, and you can delay the transfer until the seller is verified.

### How do I handle tips or variable fees in a split payment?

Add the tip to the total charge amount and adjust your transfer amounts accordingly. Store the tip amount in metadata for reconciliation.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-split-payments-between-two-users-in-stripe
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-split-payments-between-two-users-in-stripe
