The Payout Engine: Marketplace Engineering at Scale
A payout engine is the system that collects money from buyers, holds it during a transaction, and releases it to sellers or service providers on a defined schedule minus the platform fee. In marketplaces with any meaningful volume, this system needs to be idempotent, auditable, and resilient to partial failures. Getting it right early saves months of painful remediation later.
What you actually need to know
- Build idempotent payout jobs from the start. Retries happen. Double payouts are catastrophic.
- Stripe Connect covers the compliance burden on Standard and Express. Custom shifts it to you.
- Every payout record needs a clear state machine: pending, initiated, succeeded, failed.
- Payout failures need an explicit path that is visible to the user. Silently failing is not an option.
- Supporting multiple currencies adds complexity at every layer. Design for it early even if you launch single currency.
| Provider | Best for | Fee structure | Compliance handling |
|---|---|---|---|
| Stripe Connect | Most marketplaces at any stage | 0.25% per payout + card fees | High on Standard/Express |
| Adyen Marketplace | Enterprise volume, complex splits | Custom pricing above threshold | High, more configurable |
| Mangopay | EU focused marketplaces | Flat fee plus percentage | Strong EU compliance stack |
| Payoneer | Cross border freelancer payments | Fixed fees per transfer | Handles mass payouts globally |
The core argument
Marketplace founders underestimate the payout engine until they ship one. The inbound payment is straightforward. Stripe, a checkout form, a webhook. The payout side is where the complexity lives. You are moving money to multiple parties, on different schedules, in potentially different currencies, subject to different compliance obligations, while the platform takes a fee in the middle.
The naive version is a cron job that pulls unsettled balances and fires transfers. It works until it does not. A network failure partway through a run, a bank account that has been closed, a payout that was initiated twice because the job retried after a timeout. Each of these produces a support ticket that takes hours to resolve. At volume they produce legal exposure.
The right version is a small state machine. Each payout has a status. The job that initiates payouts only picks up records in the correct state. The job that marks payouts as succeeded only processes the webhook once, regardless of how many times it fires. The job that handles failures creates a state the user can see and sends a notification. None of this is architecturally complex. It just requires writing it down before writing the code.
I have helped build and audit payout engines for three marketplace clients. The pattern that fails is always the same. The team builds the happy path first, ships it, and deals with the edge cases reactively when they surface in production. The edge cases always surface. Building the state machine first is about two weeks of extra work. Retrofitting it after things go wrong in production is much more.
Designing the state machine
Payout record states
Every payout record in your database should have one of these states: pending, scheduled, initiated, succeeded, failed, or held. Pending means the funds have cleared but payout has not been scheduled. Scheduled means the payout is queued for the next batch. Initiated means the API call to the payment processor has been made. Succeeded means the processor confirmed delivery. Failed means the processor returned an error. Held means the payout is paused because the seller needs to update their account.
Idempotency keys
Every API call to initiate a payout should include an idempotency key derived from the payout record ID. If the job retries after a timeout, the processor will return the same result for the same key rather than creating a second transfer. Stripe supports this natively. Use it.
Webhook handling
The confirmation that a payout succeeded comes via webhook. Process each webhook event once. Store the event ID. If the same event arrives twice, return 200 and skip processing. The duplication happens more than you expect.
Fee calculation
Calculate the platform fee before the payout, not at the payout. Store the gross amount, the fee amount, and the net payout amount on every record. This is what you need for reconciliation, for tax reporting, and for the seller's earnings statement.
How much does it cost
| Volume per month | Stripe Connect cost | Engineering cost to build | Time to build |
|---|---|---|---|
| Under 50k USD | 0.25% plus card fees | Low, shared with billing work | 2 to 4 weeks |
| 50k to 500k USD | 0.25% plus card fees | Medium, state machine required | 4 to 8 weeks |
| 500k to 5M USD | Negotiable | High, reconciliation tooling needed | 3 to 6 months |
| Above 5M USD | Custom agreement | Very high, potentially custom infra | 6 to 18 months |
What the payout engine must handle
- Idempotent payout initiation with idempotency keys on every API call.
- Explicit failure states with notifications to the seller and a path to resolution.
- Scheduled payout batches that are atomic or resumable on failure.
- Reconciliation reports that match internal records to processor records.
- Audit log of every state transition on every payout record.
- Support for multiple currencies with exchange rate stored per payout.
- Held funds handling for KYC holds, disputes, or seller account issues.
- 1099-K and equivalent tax reporting records for each seller.
Expert opinion
The founders who build the payout engine correctly the first time treat it like a financial system, not a feature. A feature ships and iterates. A financial system has invariants that cannot be violated by a bug. The state machine is the invariant. It is not optional and it is not something you retrofit cleanly after the fact. Build it before you have volume that depends on it working.
Yashveer Singh, founder of Yashveer Labs
How this played out on a real project
A services marketplace client with buyers and sellers on both sides came to me with a payout problem six months after launch. Their payout cron job had a race condition under load. Two instances of the job ran simultaneously during a brief infrastructure incident. Roughly thirty sellers received duplicate payouts totaling about 14,000 USD. No state machine. No idempotency keys. No duplicate detection on the webhook handler.
The immediate fix was a lock at the database level on the payout job and deduplication on the webhook handler. The proper fix took three weeks: a full state machine rebuild, idempotency keys on every Stripe API call, a reconciliation script that compared internal records to Stripe's transfer log, and an ops dashboard showing payout status per seller.
The 14,000 USD was recoverable because the sellers were generally cooperative and the amounts per seller were small. The cost in engineer time and trust was not recoverable. Two sellers churned. The rebuild cost about four weeks of senior engineer time that was not in the roadmap.
For more on the broader engineering context, see why your saas should have a job queue from day one and building an operations stack without an operations team.
Common mistakes
- No idempotency keys on payout API calls. Retries produce duplicate transfers.
- Silently failing payouts. The seller notices eventually, and then it is a support crisis.
- No explicit failed state. The payout just disappears from the queue.
- Fee calculation at payout time from live data. The fee should be locked at the time of the transaction.
- Webhook processing without deduplication. Processors retry. Process each event once.
- Storing only the net payout amount. You need gross, fee, and net for every record.
- Not testing partial failure scenarios. Run the payout job and kill it partway through a batch in staging.
- Launching with multiple currencies without storing the exchange rate used. Reconciliation becomes impossible.
A 6 week plan
- Week one. Define the state machine. Draw it. Get sign-off from the team before writing code.
- Week two. Build the payout record schema with all states. Write the job that transitions pending to scheduled.
- Week three. Build the initiation job with idempotency keys. Build the webhook handler with deduplication.
- Week four. Build the failure state, seller notification, and the held state for KYC or account issues.
- Week five. Build the reconciliation report. Verify it matches Stripe's transfer log in staging.
- Week six. Load test the payout job with simulated concurrent runs. Verify the lock holds.
For more on the engineering patterns that make this work reliably, read webhooks the reliable pattern that most companies get wrong and the subscription billing stack in 2026.
FAQ
Frequently asked
- What is the difference between Stripe Connect Standard, Express, and Custom for marketplace payouts?
- How do I handle payout failures when a seller's bank account is invalid or closed?
- What is idempotency and why does it matter for payout systems?
- How do I handle payouts in multiple currencies in a marketplace operating in more than one country?
- When should I build a custom payout engine versus using Stripe Connect or a similar managed service?
- How should I structure payout schedules for a marketplace with sellers who have different preferences?
- What compliance requirements do I need to think about when processing payouts to sellers in different countries?
Author
About me and why that should matter to you
Yashveer Singh. Full stack developer. Founder of Yashveer Labs. Based in New Delhi. The reason it should matter to you is that I have actually built these systems, not just read about them. The code is on GitHub. The systems are on real URLs. The portfolio has the proof. The contact channel is Instagram. If the work needs to get done, that is how you reach me.