Implementation of Escrow Payments on Marketplace
Escrow is a mechanism for conditional funds holding: buyer pays, money is held by platform and released to seller only after delivery confirmation. This is a key trust tool on marketplaces where seller and buyer are strangers.
Why Escrow is Needed
Without escrow, buyer risks paying without receiving goods. Seller risks sending goods without payment. Escrow solves both problems: money exists, but neither party controls it until conditions met. Standard for marketplaces from Avito to Wildberries.
Implementation Options
Own escrow on platform balance — money arrives at platform's account, internal accounting of obligations to each seller. Most common for Russian marketplaces.
YuKassa for Marketplaces (Split Payments) — YuKassa holds funds and distributes them to seller accounts (seller onboarded in YuKassa) with platform commission retention. Requires seller registration in YuKassa.
Stripe Connect — equivalent for international platforms. Platform creates Connected Accounts for sellers, uses Transfer + Destination Charge. Stripe holds funds and manages payouts.
Data Model for Own Escrow
escrow_transactions (
id, order_id, buyer_id, seller_id,
amount, currency,
status: pending | held | released | refunded | disputed,
held_at, release_trigger: delivery_confirmed | auto_timeout | admin_release,
released_at, payment_id,
notes
)
-- Platform balance (summary accounting)
platform_escrow_balance (
total_held, -- sum of all active escrow
available, -- funds available for payouts
updated_at
)
Escrow Lifecycle
Buyer pays for order
↓
Funds held (status: held)
↓
┌──────┴──────────┐
Delivery Dispute opened
confirmation ↓
↓ Arbitration
release ↓ ↓
↓ refund release
To seller To buyer To seller
Automatic Release
If buyer doesn't confirm or dispute order within N days after delivery — funds release automatically. Protects sellers from situation where buyer "forgets" to confirm.
// Scheduler: every 6 hours
$autoRelease = EscrowTransaction::where('status', 'held')
->whereHas('order', function($q) {
$q->where('delivered_at', '<', now()->subDays(config('escrow.auto_release_days')));
})
->get();
foreach ($autoRelease as $tx) {
EscrowService::release($tx, 'auto_timeout');
}
Typical timeout: 7–14 days after delivery. For high-value goods — longer.
Partial Release
On partial return (goods arrived but with defect) part of amount releases to seller, part returns to buyer. Split negotiated during arbitration.
YuKassa Split Integration
// Create payment with split
$payment = $client->createPayment([
'amount' => ['value' => '5000.00', 'currency' => 'RUB'],
'transfers' => [
[
'account_id' => $seller->yookassa_account_id,
'amount' => ['value' => '4250.00', 'currency' => 'RUB'], // minus commission
'platform_fee_amount' => ['value' => '750.00', 'currency' => 'RUB']
]
],
// ... other parameters
]);
Transfer created deferred — funds not transferred to seller until explicit confirmation.
Legal Aspects
Agent scheme (platform as seller's agent) — most common legal structure for RF marketplaces. Agent contract with each seller regulates escrow conditions, payment terms and schedule. Important to set up before launch — FTA may qualify storage of others' money without contract as illegal banking.
Monitoring and Reporting
- Daily reconciliation: sum of
heldin DB must match actual account balance - Alert if discrepancy > 1%
- Escrow register by sellers for finance department
- Report on hold periods (average, maximum) — delivery problem indicator
Development timeline: 6–8 weeks for complete escrow system with payment provider integration, arbitration, and automatic release.







