3 MIN READ

Building a Marketplace on Medusa: Vendors, Splits and Payouts

Multi-vendor commerce means splitting one customer order into several vendor orders, and moving money to people who are not you. The model and the money mechanics.

BY RAHUL MEHTAUPDATED
Illustration for “Building a Marketplace on Medusa: Vendors, Splits and Payouts” — Marketplace

A marketplace is a normal store plus two hard problems: one customer order becomes several vendor orders, and money has to reach people who are not you. Everything else — vendor onboarding, dashboards, commission rules — is ordinary application work.

Get the two hard problems right and the rest follows.

The vendor module

src/modules/marketplace/models/vendor.tsts
import { model } from "@medusajs/framework/utils"

export const Vendor = model.define("vendor", {
  id: model.id().primaryKey(),
  name: model.text(),
  handle: model.text().unique(),
  status: model.enum(["pending", "active", "suspended"]).default("pending"),
  commission_rate: model.number().default(15),        // percent
  payout_account_id: model.text().nullable(),          // e.g. Stripe Connect account
  payout_schedule: model.enum(["daily", "weekly", "monthly"]).default("weekly"),
  admins: model.hasMany(() => VendorAdmin),
})

export const VendorAdmin = model.define("vendor_admin", {
  id: model.id().primaryKey(),
  email: model.text().unique(),
  vendor: model.belongsTo(() => Vendor, { mappedBy: "admins" }),
})

Link vendors to products so every product has an owner, and to orders so every vendor order has a payee. Vendor admins are separate from customers and from store staff — three distinct actor types with three different permission sets.

Splitting the order

The core mechanic. When a cart completes, group its items by vendor and create a child order per vendor, all inside the completion workflow:

src/workflows/steps/split-order-by-vendor.tsts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type Input = { order_id: string }

export const splitOrderByVendorStep = createStep(
  "split-order-by-vendor",
  async ({ order_id }: Input, { container }) => {
    const query = container.resolve("query")
    const marketplace = container.resolve("marketplace")

    const { data: [order] } = await query.graph({
      entity: "order",
      fields: ["id", "items.*", "items.variant.product.vendor.*", "currency_code"],
      filters: { id: order_id },
    })

    // One bucket per vendor represented in the cart.
    const byVendor = new Map<string, typeof order.items>()
    for (const item of order.items) {
      const vendorId = item.variant?.product?.vendor?.id
      if (!vendorId) continue
      byVendor.set(vendorId, [...(byVendor.get(vendorId) ?? []), item])
    }

    const vendorOrders = await marketplace.createVendorOrders(
      [...byVendor.entries()].map(([vendorId, items]) => ({
        vendor_id: vendorId,
        parent_order_id: order_id,
        items: items.map((i) => ({ line_item_id: i.id, quantity: i.quantity })),
        subtotal: items.reduce((sum, i) => sum + Number(i.total), 0),
      })),
    )

    return new StepResponse(vendorOrders, vendorOrders.map((o) => o.id))
  },
  async (ids: string[] | undefined, { container }) => {
    if (!ids?.length) return
    await container.resolve("marketplace").deleteVendorOrders(ids)
  }
)

The customer keeps one order and one payment. Vendors each get a fulfilment-scoped order. Compensation removes the vendor orders if a later step fails, so a rejected checkout does not leave three vendors with phantom work.

Money

The part where mistakes are regulatory rather than merely embarrassing.

Do not collect money and pay vendors from your own account unless you have taken advice about money-transmission licensing in every jurisdiction you operate in. Use a platform payment product built for it — Stripe Connect, Adyen for Platforms, or a regional equivalent — where the processor handles the split and the compliance.

With Stripe Connect the shape is:

  1. Vendors onboard through Connect and get an account id.
  2. At payment, the charge specifies transfers to each vendor account minus commission.
  3. Stripe handles payouts on the vendor's schedule.
  4. Refunds reverse the corresponding transfer.

Commission is computed at split time and stored on the vendor order, so a later rate change does not rewrite history.

Vendor access

Vendors need a scoped API and dashboard. Two rules, and they are non-negotiable:

Scope every query by vendor id, in middleware, not in each handler. One forgotten filter leaks another vendor's orders, and that is a breach rather than a bug.

src/api/middlewares.tsts
import { defineMiddlewares, authenticate } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/vendor/*",
      middlewares: [authenticate("vendor", ["session", "bearer"])],
    },
  ],
})

Give vendors only what they need: their orders, their products, their payouts, their fulfilment actions. Not customer contact details beyond what shipping requires, and never store-wide analytics.

Returns and refunds

The part that is always underestimated.

  • Who authorises a return? The vendor, the platform, or either — decide and encode it.
  • Who pays return shipping? Depends on reason code. Encode that too.
  • How is the refund split? The vendor's share reverses; whether your commission also reverses is a policy decision with real revenue impact.
  • What if the vendor has already been paid out? Then you are clawing back from a future payout, and that needs a negative balance concept from day one.

Design the return flow at the same time as the order flow. Retrofitting clawbacks into a payout system that assumed money only moves outward is genuinely painful.

Effort

CapabilityTypical effort
Vendor module and onboarding2 weeks
Order splitting1–2 weeks
Connect integration and payouts2–3 weeks
Vendor dashboard3–4 weeks
Returns and clawbacks2 weeks
Commission rules and reporting1–2 weeks

Eleven to fifteen weeks for a functioning marketplace. The vendor dashboard is consistently the largest single piece and consistently underestimated.

Vendor onboarding

The unglamorous work that determines whether vendors ever list anything.

Stage the requirements. Do not ask for tax details and bank information before a vendor has seen the dashboard. Let them sign up, explore, and add products in draft; require payout details only before their first listing goes live.

Automate identity and payout onboarding. Stripe Connect's hosted onboarding handles KYC and bank details and keeps that data off your systems entirely, which is both faster and a smaller compliance surface.

Make the first product easy. A guided flow for one product beats a bulk CSV import as a first experience. Offer the import to vendors who have already listed something.

Review before publishing. A status field on the vendor and on products, with an approval step, prevents your storefront from becoming a surprise. Automate what you can — image quality, required fields, prohibited categories — and review the rest.

Commission models

ModelWorks forWatch
Flat percentageMost marketplacesSimple, may not fit all categories
Per-category rateMixed margin categoriesMore rules to maintain
Tiered by volumeRewarding large vendorsRecalculating retroactively
Fixed fee plus percentageLow-value itemsFeels punitive on cheap goods
Subscription plus lower rateCommitted vendorsTwo billing systems

Store the rate that applied on the vendor order, not just on the vendor. When you change commission next year, historical orders must keep their original rate — otherwise every past payout report changes retroactively, which is a reconciliation nightmare and, depending on your vendor agreement, a contractual problem.


Marketplaces are among the most demanding things we build. Ask for a scope before committing to a date.

Frequently asked questions

Can Medusa be used to build a marketplace?

Yes, and its module system suits it well — vendors, vendor orders and payouts become custom modules linked to core commerce entities, with order splitting handled in the completion workflow. Expect roughly three months for a functioning marketplace.

How do I split an order between vendors in Medusa?

Group line items by the vendor that owns each product and create a vendor order per group inside the completion workflow. The customer keeps a single order and payment; vendors receive fulfilment-scoped orders.

How should marketplace payouts be handled?

Through a platform payments product such as Stripe Connect, which splits the charge and handles payouts and compliance. Collecting funds and paying vendors from your own account can constitute money transmission and needs legal advice first.

How do vendors access their own data?

Through a scoped API and dashboard, with vendor filtering applied in middleware rather than in individual handlers. A single missing filter exposes another vendor's data, so scope it once at the boundary.

How do refunds work in a marketplace?

The refund reverses the customer payment and the corresponding vendor transfer. If the vendor has already been paid out you need a clawback against future payouts, which means designing negative balances into the payout model from the start.

How long does it take to build a marketplace on Medusa?

Eleven to fifteen weeks for vendor management, order splitting, payouts, a vendor dashboard and returns. The vendor dashboard is usually the largest piece and the one most often underestimated.

[ Keep reading ]