auditmyapp

Notes / Stripe Connect

Refunding a buyer after the seller has been paid, on Stripe Connect

On a marketplace using separate charges and transfers, a refund does not touch the seller's money. I built a test marketplace in a Stripe sandbox to watch exactly what happens.

If you run a marketplace on Stripe Connect, there is a question worth answering before your first refund rather than after it: when you refund a buyer, where does the money come from?

I wanted to watch it happen rather than read about it, so I built a small marketplace in a Stripe sandbox. Two test sellers, onboarded properly. A buyer paying $2,500 for a watch. The money held by the platform until the sale is confirmed, then $2,250 released to the seller and $250 kept as commission. Then I refunded the buyer.

What actually happened

The buyer got their $2,500 back. The platform's balance dropped by $2,500. And the seller still had their $2,250.

Nothing was broken. Stripe did exactly what it was asked, which was to refund the charge. But under the model most marketplaces use, the buyer's payment and the payment to the seller are two separate things, and refunding one does nothing to the other. Until the platform does something more, it has paid the buyer back and paid the seller, and it is short by the seller's share.

The fix is a transfer reversal, which pulls the seller's share back. When I created one, the $2,250 moved back to the platform and the books balanced again.

Why Stripe doesn't do it for you

Stripe Connect has a few ways to move money between buyers and sellers. With destination charges, the payment and the seller's share are linked, and a refund can reverse the seller's share in the same step. With separate charges and transfers, the payment lands with the platform and you decide when to pay the seller. That flexibility is why marketplaces like it: you can hold funds until an item has been authenticated or delivered. The price is that keeping the two in step is your job.

Doing it properly

// refundRequestId: one per refund request, so each partial refund gets its own key
const refund = await stripe.refunds.create(
  { payment_intent: order.paymentIntentId, amount: refundAmount },
  { idempotencyKey: `refund-${order.id}-${refundRequestId}` },
);

// Pull back the seller's proportional share
const reversalAmount = Math.round((order.sellerAmount * refundAmount) / order.amount);

await stripe.transfers.createReversal(
  order.transferId,
  { amount: reversalAmount, metadata: { order_id: order.id, refund_id: refund.id } },
  { idempotencyKey: `reversal-${order.id}-${refundRequestId}` },
);

Three details in there are where real systems go wrong:

Disputes work the same way

I ran a second test using Stripe's test card that triggers a dispute. The disputed amount, plus a dispute fee, came out of the platform's balance. The seller kept their share. Whether you then reverse it is a policy decision, and it is much better made on purpose than discovered in your bank balance.

The hard case: the seller has already withdrawn the money

If the seller's share has already been paid out to their bank, a reversal can leave their Stripe balance negative. If your platform is set up as responsible for losses, which is common for marketplaces, recovering that is ultimately your problem. That is a good argument for holding funds until the risk of a refund has mostly passed, and for knowing exactly what your refund policy says before a buyer asks.

A short checklist for marketplace payments

Marketplace payment flows are where small gaps become real money. If you are close to launch, this is the part worth having checked before real buyers and sellers move through it.