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:
- The idempotency key is per refund request, not per order. Key it on the order alone, and a second partial refund quietly returns the first one instead of creating a new one.
- Partial refunds need partial reversals. Refund half the order and you should reverse half the seller's share. Not all of it, and not none of it.
- Refund and reversal are two calls, so one can succeed while the other fails. Record each step, and have a way to retry the reversal on its own.
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
- Every refund path makes an explicit decision about reversing the seller's share.
- Idempotency keys on every payment, transfer, refund and reversal, scoped to the specific request.
- Transfers paid from the order's own charge, using
source_transaction, so a seller is never paid out of other buyers' money. - A
transfer_groupon each order, so you can always trace which payment funded which payout, and what was refunded. - A written policy for disputes, and for sellers who have already been paid out.
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.