Mercur uses Stripe’s Separate Charges and Transfers model. The platform collects the full payment from the customer, then creates transfers to each seller’s connected account. This makes the platform the Merchant of Record, responsible for VAT, disputes, and chargebacks.
Prerequisites
- A Stripe account with Connect enabled
- Stripe Secret API key and Publishable API key
- A running Mercur project (installation guide)
- Node.js 20+
Architecture overview
The payment lifecycle in a Mercur marketplace flows through two distinct Stripe integrations: The two integrations serve different purposes:| Integration | Package | Purpose | Webhook endpoint |
|---|---|---|---|
| Customer payments | @medusajs/medusa/payment-stripe | Charges customers at checkout | /hooks/payment/stripe_stripe |
| Seller payouts | @mercurjs/payout-stripe-connect | Transfers funds to sellers | /hooks/payout |
Stripe Dashboard setup
Enable Connect
In your Stripe Dashboard, go to Settings → Connect settings and enable Connect. If you’re starting fresh, Stripe will walk you through an onboarding flow to activate your platform account.
Find your API keys
Go to Developers → API keys. You’ll need:
- Secret key — starts with
sk_test_(test mode) orsk_live_(production) - Publishable key — starts with
pk_test_orpk_live_
Configure Medusa payment provider
The Medusa payment provider handles customer-facing charges. Add it to yourmedusa-config.ts:
Provider options
| Option | Type | Description |
|---|---|---|
apiKey | string | Stripe secret API key |
webhookSecret | string | Signing secret for the payment webhook endpoint |
capture | boolean | Set to false for manual capture (required for marketplace split flow) |
automatic_payment_methods | boolean | Enable Stripe’s automatic payment method detection |
paymentDescription | string | Optional description shown on customer’s bank statement |
After configuring the provider, enable Stripe as a payment method in your Admin Panel: Settings → Regions → Edit region → Payment providers → Stripe.
Configure Mercur payout provider
The payout provider handles transfers from the platform to seller connected accounts. Add it alongside the payment provider inmedusa-config.ts:
Payout provider options
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Stripe secret API key |
webhookSecret | string | — | Signing secret for the payout webhook endpoint |
accountValidation | object | See below | Controls when a connected account is considered ACTIVE |
Account validation options
These options determine what conditions a Stripe connected account must meet before Mercur marks it asACTIVE and allows payouts:
| Option | Type | Default | Description |
|---|---|---|---|
detailsSubmitted | boolean | true | Require Stripe to mark onboarding details as submitted |
chargesEnabled | boolean | true | Require the account to be enabled for charges |
payoutsEnabled | boolean | true | Require the account to be enabled for payouts |
noOutstandingRequirements | boolean | true | Treat any pending Stripe requirements as a restricted account |
requiredCapabilities | string[] | [] | Require specific Stripe capabilities to be active (e.g. ["card_payments", "transfers"]) |
Payout module options
The payout module itself accepts timing options that control the capture and payout pipeline:| Option | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disable automatic capture checks and daily payout jobs |
authorizationWindowMs | number | 604800000 (7 days) | How long a payment authorization remains valid |
sellerActionWindowMs | number | 259200000 (72 hours) | How long sellers have to accept/fulfill before cancellation |
captureSafetyBufferMs | number | 86400000 (24 hours) | Safety margin before authorization expiry — capture happens before authorization expiry - buffer |
requiredFulfillmentStatus | string | "fulfilled" | Minimum fulfillment status before an order is eligible for capture |
Set up webhooks
You need two separate webhook endpoints in Stripe — one for payment events, one for payout events.Add the payment webhook (Medusa)
In the Stripe Dashboard, go to Developers → Webhooks → Add endpoint.
- Endpoint URL:
https://{your-backend-url}/hooks/payment/stripe_stripe - Events to subscribe to:
payment_intent.succeededpayment_intent.amount_capturable_updatedpayment_intent.payment_failedcharge.refunded
STRIPE_WEBHOOK_SECRET in your .env.How the payment flow works
Here’s a concrete example. A customer buys items from two sellers:- Seller A: €40 (product)
- Seller B: €30 (product)
- Shipping: €10
- Cart total: €80
Step 1 — Authorize payment
At checkout, a singlePaymentIntent is created for €80 with capture_method: "manual". The customer authenticates once (SCA-compliant). No money moves yet — the funds are held on the customer’s card.
Step 2 — Split orders
Mercur’scompleteCartWithSplitOrdersWorkflow groups items by seller and creates separate orders — one for Seller A (€40) and one for Seller B (€30), plus shipping allocation.
Step 3 — Seller acceptance and fulfillment
Each seller reviews and fulfills their order through the Vendor Portal. The payout module’ssellerActionWindowMs (default: 72 hours) defines how long sellers have to act.
Step 4 — Capture payment
Once orders meet therequiredFulfillmentStatus (default: "fulfilled"), the platform captures the authorized payment. The capture-check job runs automatically and respects the captureSafetyBufferMs to ensure capture happens before authorization expiry.
Step 5 — Commission calculation and transfers
After capture, Mercur calculates commission for each order and creates Stripe Transfers for the net amounts:source_transaction and grouped by transfer_group (the order ID).
Step 6 — Bank payouts
Stripe automatically pays out connected account balances to sellers’ bank accounts on the configured payout schedule. Mercur tracks payout status changes via theaccount.updated webhook.
Seller onboarding
When a seller creates a payout account, the following lifecycle begins:-
Account creation — Mercur calls
stripe.accounts.create({ type: "express" }), creating a Stripe Express connected account. The payout account starts inPENDINGstatus. -
Onboarding link — The seller receives a Stripe-hosted onboarding URL via
stripe.accountLinks.create(). They complete identity verification, bank account setup, and any required compliance steps directly on Stripe. -
Webhook activation — When the seller completes onboarding, Stripe sends an
account.updatedwebhook. The provider evaluates the account against theaccountValidationoptions and transitions the status:- All validation checks pass →
ACTIVE - Missing requirements or disabled reason →
RESTRICTED - Disabled reason starts with
rejected.→REJECTED
- All validation checks pass →
-
Ongoing monitoring — Stripe may send additional
account.updatedevents if requirements change. The provider re-evaluates and updates the status accordingly.
Transfers vs payouts
Stripe uses two distinct concepts for moving money, and it’s important to understand the difference:| Transfer | Payout | |
|---|---|---|
| What it does | Moves funds from platform balance to connected account balance | Moves funds from connected account balance to seller’s bank account |
| Speed | Instant ledger movement | 1–3 business days (varies by country) |
| Status lifecycle | None — transfers are immediate | pending → in_transit → paid / failed |
| Who triggers it | Mercur (via stripe.transfers.create()) | Stripe (on the connected account’s payout schedule) |
| Mercur tracking | Transfer created with status PAID immediately | Status tracked via webhooks |
createPayout method is called, the Stripe Connect provider creates a Transfer (platform → connected account). The actual bank payout (connected account → bank) happens automatically on Stripe’s schedule.
Refunds and reversals
Refunding a charge does not automatically reverse the associated transfers. These are two separate operations:- Refund the PaymentIntent — Returns funds to the customer’s payment method
- Reverse the Transfer(s) — Claws back funds from the connected account(s)
Refund and transfer reversal orchestration is an area where custom workflow logic may be needed depending on your business rules. See Stripe’s documentation on reversing transfers for the API details.
EU/EEA considerations
Merchant of Record
Using Separate Charges and Transfers makes the platform the Merchant of Record. This means:- The platform collects and remits VAT
- The platform handles consumer disputes and chargebacks
- The platform is responsible for PSD2 compliance
Strong Customer Authentication (SCA)
SCA is enforced at authorization time, not capture time. Since the customer authenticates once during checkout, no additional SCA step is needed when the platform captures later. The single authorization at checkout satisfies the SCA requirement for the entire order.Authorization windows
Card authorizations in Europe typically last 7 days. Design your seller acceptance workflow to complete well within this window. The payout module options give you control over the timing:Testing
Test mode
Use Stripe test mode keys (sk_test_, pk_test_) during development. All connected accounts and payments created in test mode are isolated from production.
Test card number: 4242 4242 4242 4242 (any future expiry, any CVC)
Webhook forwarding with Stripe CLI
Since webhooks need to reach your local machine during development, use the Stripe CLI to forward events. You need two separate listeners — one for each webhook endpoint:Testing the full flow
- Create a seller and complete Stripe’s test onboarding flow
- Add products from the seller to a cart
- Complete checkout using the test card
- Verify orders are split by seller in the Admin Panel
- Fulfill the orders through the Vendor Portal
- Confirm transfers appear in the Stripe Dashboard under the connected account
FAQ
Why do I need two webhook endpoints with two secrets?
Why do I need two webhook endpoints with two secrets?
They belong to two different integrations: the payment webhook feeds Medusa’s payment provider (charges, captures, refunds), while the payout webhook feeds Mercur’s payout provider (connected-account status, transfer status). Each endpoint verifies its own signing secret — combining them silently breaks whichever side’s signature doesn’t match.
Why must capture be set to false on the payment provider?
Why must capture be set to false on the payment provider?
The marketplace flow authorizes at checkout and captures later, after sellers fulfill. Automatic capture would take the money before the split-order pipeline (fulfillment checks, commission, payouts) has run —
capture: false hands that timing to the payout module’s capture job.A seller finished Stripe onboarding but is still not ACTIVE — why?
A seller finished Stripe onboarding but is still not ACTIVE — why?
Check the
accountValidation options: by default the account must have details submitted, charges and payouts enabled, and no outstanding requirements. Stripe often adds follow-up requirements (e.g. extra KYC) after initial onboarding — the account shows as RESTRICTED until they’re cleared.Who handles VAT, disputes, and chargebacks?
Who handles VAT, disputes, and chargebacks?
The platform. Separate Charges and Transfers makes the platform the Merchant of Record — see EU/EEA considerations for what that entails.
Next steps
Set up seller payouts
Hands-on: one seller from onboarding to a settled payout.
Payout
The pipeline reference — statuses, jobs, and webhook events.