Every marketplace hits the same question in its first month: between "the buyer paid" and "the seller delivered", who is holding the money?
If the seller is paid immediately, the buyer carries all the risk and your support inbox fills up. If the platform holds it, you are now the one holding other people's money, with everything that implies. This post walks through the two ways developers usually solve it, with code, and the constraints that only show up once you are in production.
Option A: hold funds with Stripe Connect
Start with what Stripe itself says. Its Connect docs state that escrow has a precise legal definition and that Stripe doesn't provide escrow services or support escrow accounts. Escrow services also appear on Stripe's restricted-businesses list. What Connect does give you is control over when money moves.
The usual pattern is separate charges and transfers: the buyer pays the platform, and the seller's share is transferred once delivery is confirmed.
// 1. The buyer pays your platform
const intent = await stripe.paymentIntents.create({
amount: 25000, // $250.00
currency: 'usd',
transfer_group: 'order-1042',
});
// ...the seller ships, the buyer confirms...
// 2. Move the seller's share to their connected account
await stripe.transfers.create({
amount: 22500, // your commission stays on the platform
currency: 'usd',
destination: 'acct_seller123',
transfer_group: 'order-1042',
source_transaction: chargeId, // waits until the charge's funds are available
});
The alternative is to transfer straight away and use manual payouts on the connected account, so the money sits in the seller's Stripe balance until you release the payout.
The constraints that matter:
- How long you can hold. Manual payouts can be delayed up to 90 days in most countries (two years in the US, 10 days in Thailand). A plain card authorization lasts 7 days for online payments, and extended authorization (about 30 days) needs IC+ pricing and is generally meant for cases where the final amount isn't known.
- It's still your balance. With separate charges and transfers, the funds are in your platform's Stripe balance. Stripe recommends this charge type only when you are responsible for connected accounts' negative balances.
- Chargebacks outlive delivery. Stripe notes that cardholders can typically dispute within 120 days, and with destination charges or separate charges and transfers your platform balance is debited for the disputed amount and fee. A buyer can dispute after the seller has delivered and been paid.
- Stripe's own guidance is to hold funds only with a clear purpose and a commitment to release them when a condition is met, and to talk to a legal advisor rather than hold funds arbitrarily.
None of that makes Connect wrong. For card payments with short fulfilment it is the obvious choice. But you are building escrow-like behaviour on a platform that tells you it isn't escrow, and your balance is the shock absorber.
Option B: let a contract hold the money
If your buyers can pay in stablecoins, the holding can move off your books entirely: the payment locks in a smart contract that can only pay the buyer or that order's seller. Here is the same order through the Vaultion escrow API, which takes no account or API key and is called from your server:
// Runs on your server. Never in the browser.
const res = await fetch('https://checkout.vaultion.org/v1/stores/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
store: { url: 'https://yourplatform.com', name: 'Your Platform', email: 'seller@example.com' },
orderRef: 'order-1042', // same ref returns the same checkout, so retries are safe
name: 'Logo design: final files',
priceMinor: 25000, // $250.00
escrow: 'required', // escrow only for this order
receiving: { evm: '0xSellerWalletAddress' }, // this order's seller
returnUrl: 'https://yourplatform.com/orders/1042',
}),
});
const session = await res.json();
// Redirect the buyer to 'https://checkout.vaultion.org' + session.checkoutPath
The buyer funds the escrow from their own wallet on a hosted page. Your backend then treats the order as a small state machine by reading the session status:
const { status } = await fetch(
'https://checkout.vaultion.org/v1/stores/sessions/' + session.sessionId
).then((r) => r.json());
switch (status) {
case 'escrow_funded': /* money is locked: tell the seller to ship */ break;
case 'escrow_disputed': /* hold fulfilment until it is ruled */ break;
case 'escrow_released': /* the seller is paid */ break;
case 'escrow_refunded': /* the buyer got the money back */ break;
}
The seller is paid when the buyer releases, or can claim it once the review period ends, so a buyer who goes silent doesn't strand the money. A confirmed on-chain payment can't be charged back.
What you give up, honestly:
- Buyers need crypto. USDC or USDT on one of six networks. It is a second rail next to cards, not a replacement for them.
- No webhooks yet. You poll the status (every 30 to 60 seconds while the buyer is on the checkout, then every few minutes).
- No sandbox, and a $50 minimum per escrow.
- No platform commission inside the escrow. The only fee in it is Vaultion's, paid by the buyer (4% below 500 USDC, 3% from 500, 2% from 5,000), so you charge your own fee separately.
- Disputes go to a Vaultion reviewer. It's free, but it's not decentralized: you are trusting Vaultion's reviewers. A separate guardian can pause a ruling but never redirect funds.
Side by side
| Stripe Connect hold | Escrow contract | |
|---|---|---|
| Who holds the money | Your platform's or the seller's Stripe balance | A smart contract |
| Longest hold | 7-day auth; payouts up to 90 days (most countries) | Until release or the review period ends |
| Buyer reversal after payout | Card disputes, typically up to 120 days | None; disputes are settled before release |
| Platform commission | Built in (transfer less than you charged) | Charged separately |
| Payment methods | Cards and local methods | USDC and USDT |
| Integration | Charges, transfers, payouts, webhooks | One POST per order, poll the status |
Which one
Card buyers and delivery inside a week: authorize and capture. Card buyers and longer fulfilment: Connect with separate charges and transfers or manual payouts, plus legal advice and a chargeback plan. Buyers who pay in crypto, cross-border deals, or high-ticket orders between strangers: let a contract hold it. A lot of marketplaces will end up running both.
If you want the second route, the flow, pricing and limits are on the escrow for marketplaces page, with a link through to the API reference.
Disclosure: I work on Vaultion, the escrow API in Option B. Stripe details are from Stripe's own documentation as of October 2026; check the current docs before you build. This post was drafted with AI assistance.
Top comments (0)