Skip to main content
Spree fully supports multi-vendor marketplaces out of the box: sellers are first-class records, orders split per seller at completion, robust commission engine, and a payout ledger with pluggable payout providers. So building a marketplace doesn’t require weeks or months of development, it’s just configuring your Spree application to admit sellers, review their submissions, and pay them. In most cases you don’t need to customize Spree or write any code at all. This guide is a walkthrough of all elements that make a marketplace, and how to configure them.

What you get

  • Sellers — onboard sellers with an operator-configured requirements checklist, review submissions, and approve them for selling.
  • Order splitting — a customer checks out once; completion produces per-seller orders grouped under one purchase.
  • Commissions — the platform’s cut computed per seller, with EU commission taxation handled.
  • Payouts — a transfer and payout ledger, with Stripe Connect payouts (Express onboarding, on-fulfillment transfers) shipping in the open-source monorepo.
  • Seller operations — sellers work through their own dedicated API surface, scoped so a seller only ever sees their own trade.

Before you start

You need a running Spree 6 project and admin credentials. The marketplace surfaces are: To try the flow quickly, you can seed a sample seller with an owner account, a pending invitation and a fund ledger:
The task refuses to run outside development and test, because it writes a known password. It signs in at seller@example.com / spree123 — override with SELLER_EMAIL, SELLER_PASSWORD and SELLER_NAME.

Configure the store for marketplace trading

There is no single “marketplace mode” switch. A store becomes a marketplace when it has sellers; what you configure up front is how strictly it admits them and how it pays them.

Payout settings

Three store preferences decide how sellers get paid. All three are writable through the Admin API, and they appear in the dashboard under Settings → Marketplace.
A mistyped provider key is refused at write time rather than silently ignored, so you cannot end up with a bookkeeping-only ledger and no indication that your choice was dropped. Ask the API which providers are registered before you set one:

Admission and review settings

Four more preferences decide who gets to trade and what they hear from you. They sit on the same Settings → Marketplace screen, and are writable through the Admin API like the payout ones.
Turning either auto-approve preference on removes a human decision from your marketplace. auto_approve_sellers lets any applicant who ticks every box start trading; auto_approve_seller_products puts their listings on sale without review. Both are appropriate for an invite-only marketplace where you already trust the sellers, and dangerous for an open one.
Commission tax is stored as a fraction — 0.23 means 23%. The dashboard asks for a percentage and converts, so you only meet the fraction through the API. See Tax on commission for when it applies and what overrides it.

Define what a seller must do before trading

A seller requirement is one thing the marketplace asks of a seller before it will let them trade. The operator composes the checklist from registered kinds — rows are configuration, and only a genuinely new kind of check needs code. The checklist is enforced in exactly two places: when the seller asks to be reviewed, and when the operator approves them. Nothing else consults it.

The kinds that ship

Thirteen kinds are registered by core. Seven of them are provisioned into a new store’s default checklist, in the order a seller meets them. The first ten are computed: Spree reads the seller’s own data and answers for itself, so there is nothing for the seller to submit. The last three take a submission — a row recording what the seller said and when — and the bottom two wait for someone to accept it. Four kinds may be configured more than once per store, because their meaning comes from the operator’s own wording: attestation, operator_review, document and policy. Those four require a name. The rest are one per store.
The generic kinds are deliberately absent from the default checklist. An attestation or a document means nothing until the operator has written what they are asking for, so Spree does not guess.

Build the checklist from the registry, never a hardcoded list

Always read the kinds from types() rather than shipping your own list — a marketplace’s own registered kinds then appear for free. The endpoint returns each kind’s preference_schema, which is what a configuration form renders from.
Each kind takes its own configuration through preferences. accept_terms carries terms_body, terms_url and terms_effective_from — setting that date is how you ask everyone again after rewriting the terms, since anyone who accepted before it falls back to unmet. minimum_products carries minimum_count. complete_profile carries require_about, require_logo, require_cover_photo and require_contact_email. Two flags govern every row regardless of kind. required: false makes a line advisory — it appears on the seller’s checklist but does not gate approval. active: false retires it without deleting the submissions that answered it. Position is the order sellers work through, and the list is drag-ordered in the dashboard.
type is write-once. The API strips it on update, because a saved row’s submissions answered the old kind. To change a kind, delete the row and create a new one.
Read more: Onboarding requirements for the model, and Seller Requirements for the operator’s screen.

Invite a seller and approve them

A seller moves through its life on the marketplace by workflow, never by assigning a status. Each transition is its own Admin API action because each carries its own arguments, sends its own mail and runs its own extension hooks — mass assignment would skip all three.

Create and invite

Creating a seller through the workflow provisions a stock location for them, which is where their inventory lives and where customer returns land. A seller without one cannot finish onboarding, so this is not a step an operator can forget.
The invited person accepts through the seller panel — the emailed link carries a prefixed ID and a token, which together are the credential. Accepting creates the membership and starts onboarding, so the seller reaches onboarding without the operator doing anything more. Re-inviting is deliberate: invitations expire, and the first one goes to the wrong address often enough. What cannot be re-opened is a seller already trading or already turned away. role_id names a role the seller owns; leave it out and they accept into the seller’s own admin role. A seller’s roles are the same permission machinery the store’s back office uses, pointed at the seller — see Staff & Roles.

Review submissions

The two reviewed kinds (operator_review, document) leave a submission for someone to decide. Accepting one satisfies that line of the checklist; rejecting sends it back with a note the seller reads.
A submission is pending, accepted, rejected or waived. A waiver is the operator recording that they dealt with this outside the marketplace — it reads as met without pretending the seller did it. Submissions accumulate rather than overwrite: the latest row for a (seller, requirement) pair decides the standing, and the ones before it are the record of how you got there. Uploaded documents are stored privately and served only through the admin and seller branches. They are business registrations and identity documents, and Spree identifies them from their bytes rather than trusting the uploaded filename — a script named certificate.pdf is refused.

Approve, suspend, reject

Approval is refused while a required requirement is unmet, unless override_requirements says the operator means it. The override is recorded on the seller.approved event along with what was outstanding — the operator can step over the checklist, but not without saying so. Suspend and reject are not the same thing. Reject turns away an applicant who never traded; it refuses an approved seller outright. Suspend halts a trading seller: their catalog stops selling, their record and history stay, and approve is the way back. A suspended seller being reinstated is not re-measured against the checklist, because they were admitted once already. A seller can also take themselves off sale without any operator involvement, by setting holiday_mode_until. The catalog stays visible; what stops is selling. Read more: The seller lifecycle, Onboarding, Managing sellers.

Review what sellers list

Products belong to sellers through seller_id on Spree::Product. A product with no seller is the marketplace’s own. A marketplace adds two statuses to the product lifecycle that a single-merchant store never sees: proposed and rejected. A seller never assigns a status — they ask, and the marketplace decides. A seller can always take their own listing down — that is not a review decision. Putting one up is.

How it relates to the submission record

The product’s status is the operational truth; a Spree::ProductSubmission row is the record of how it got there — who asked, who decided, when, and what they told the seller. Its statuses are pending, approved, rejected and withdrawn. Rows accumulate: a seller sent back three times leaves three rows, and the latest one is live. Taking a listing back to draft before anyone ruled on it closes the open row as withdrawn, so a pending row always means the marketplace still owes an answer rather than “abandoned”.
A rejection reason belongs on the submission, never on the product. A seller can write their own product’s metadata, so a note kept there is erased the next time they save. Send it as reason to the reject endpoint and Spree puts it where the seller cannot overwrite it.

Both sides of the review

Filter the review queue with a Ransack query on status: adminClient.products.list({ status_eq: 'proposed' }). With preferred_auto_approve_seller_products on, submitting chains straight into approval. The submission row still gets written and carries an auto_approved marker, so a blank reviewer reads as “this store does not review listings” rather than as a lost name. There is no bulk route onto active for sellers, and that is deliberate: reaching it is the operator’s decision on one listing at a time. Read more: Seller submissions, Seller products.

Set what the marketplace charges

Commission is what the marketplace takes from a sale. You configure rates; Spree records what was actually charged as immutable commission lines, frozen when the order was placed.

Rates and precedence

The list is the precedence. Rates are walked top-down and the first whose targeting matches the sale wins. A rate with no rules matches every sale, so anything below it is unreachable — the marketplace default belongs at the bottom of the list. A new rate is placed at the top by default, ahead of anything more general already there.
A rate is either percentage (a share of the sale) or fixed (a flat fee). A flat fee states its amounts per currency — a rate is skipped for a currency it names no amount in, so that sale falls through to the next matching rate rather than being charged a converted figure nobody set. Rules ride the regular payload as rules: [...], and the server replaces the rate’s rules with exactly what it is sent. Every rule must hold for the rate to apply, and a rule naming several records means any of them — so “(Cameras OR Audio) AND that seller” is a category rule holding two ids beside a seller rule holding one.

Lines are read-only

There is no write path. Correcting a charge is a reversal, not an edit. In the EU the fee is a separate supply from the sale, so it is taxed separately — that is what preferred_default_commission_tax_rate from step 1 is for, and a rate or a tax provider can name its own. Read more: Commissions covers the four rule types, gross-versus-net, how delivery is treated, currency floors and caps, and commission tax in full. Commission rates is the operator’s screen.

Understand what a split checkout produces

This is the part that makes a marketplace different from a shop, and a storefront has to handle it. A customer fills one basket, enters one address, and pays once. But each seller needs their own order — they fulfil separately, get paid separately, and must never see each other’s business. So at completion, a checkout spanning several sellers becomes an order group: one container holding one order per seller. The single payment is apportioned across the child orders as payment splits, so each seller’s share of one charge is recorded exactly — which is what makes per-seller refunds and settlement possible later. Two details worth knowing:
  • Group totals are added up, not divided. The group’s total is the sum of its children, so it always agrees with them.
  • Delivery and order-level fees are shared out by item value, so a seller whose goods made up most of the basket carries most of the delivery charge.
A storefront must handle the possibility of an order group. Completing a cart yields either one order or a group, depending on how many sellers the basket reached. A customer’s order history should show the group as one purchase rather than confronting them with three orders they don’t remember placing separately.
A single-seller checkout produces no group at all — just the order, exactly as a normal store does. Nothing about a marketplace changes the shape of a single-seller sale. Operators can read groups back; there is no write path, because everything an operator acts on lives on the orders inside them.
Read more: One checkout, several sellers, Orders.

Pay your sellers

The fund ledger has two levels, and both are written by fulfilment and by the scheduled sweep rather than by a caller. A seller’s balance is derived from the two rather than stored, per currency, because nothing is ever converted: a seller trading in two currencies accrues two balances and is paid twice. Only settlements known to have completed count against the balance, so one whose outcome was never established still reads as owed — the honest answer while nobody knows, and it cannot be paid twice by mistake.

The sweep jobs

Two recurring jobs keep the ledger moving. A project created with create-spree-app schedules both already:
server/config/recurring.yml
On an existing project, or one running a scheduler of its own, confirm both are scheduled. A marketplace that never runs the sweep never pays a seller, and nothing reports the omission.

Settling by hand

A seller on the manual interval is skipped by the sweep. That is what the interval means: the operator decides when. The same endpoint also pays any seller early.

Payout providers

A provider is a stateless class registered in Spree.payout_providers and chosen per store with preferred_payout_provider. Providers that need a seller to hold an account with them answer requires_payout_account? and implement onboarding_url — which is what the payout_account requirement from step 2 drives. Those links are short-lived and single-use, so the seller panel asks for a fresh one at the moment of clicking rather than minting one while drawing the checklist. A connected provider confirms settlements through POST /api/v3/webhooks/payouts/:payment_method_id — separate from the payment webhook, because providers scope seller-account events to their own subscription and signing secret. Read more: Payouts for the ledger, Stripe Connect for marketplaces for the shipped provider, Seller payouts to write your own, and Payouts settings for the operator’s screen.
Refund clawbacks and netting across settlements, reconciliation, KYC operations and seller tax reporting (DAC7) are Spree Enterprise.

Stand up the seller panel

Sellers get their own application — never access to the marketplace’s dashboard. It is a separate React SPA against a separate API, shipped as @spree/seller-dashboard. It shares its foundations with the admin dashboard: components from @spree/dashboard-ui, framework pieces (registries, providers, hooks) from @spree/dashboard-core. The two panels therefore stay visually and behaviourally consistent, and anything you learn customizing one applies to the other.

Scaffold and run it

The panel ships as a shell you render from a thin host app that you own. Two ways to get one:
A new project always gets both admin SPAs, so the starter’s Docker image bakes both in; a store that never invites a seller simply leaves /sellers unvisited. Either way the host app lands at apps/seller-dashboard/ with its own package.json, Vite config and a src/plugins.ts for your customizations. Then:
It serves on port 5174 (the admin dashboard is on 5173, so both run side by side). .env.local holds one setting — the Rails server the dev proxy forwards /api to:
apps/seller-dashboard/.env.local
Do not set VITE_SPREE_API_URL in development. It switches the SDK to absolute cross-origin URLs and bypasses the proxy, which breaks the seller refresh-token cookie: the cookie rides under SameSite=Lax, and that only works while the panel is same-origin with the API.

What sellers get out of the box

Sellers cannot see other sellers, the marketplace’s own catalog, or anything belonging to the store at large.

Deploying it

By default the seller panel, same as dashboard is build with Spree docker image and is server via /sellers path. You can also deploy it as a static bundle on a CDN or static host. The entire process is baked into the default Dockerfile which you get when you scaffold a new project with create-spree-app. The spree build --production command builds both panels and bakes them into the image, so you can run it as-is. This also automatically resolves CORS issues, since the panel is served from the same origin as the API.

Customizing it

src/plugins.ts in your host app is where customizations are registered — the same defineDashboardPlugin API the admin dashboard uses, because both panels sit on @spree/dashboard-core.
apps/seller-dashboard/src/plugins.ts
Import everything from @spree/seller-dashboard — it re-exports both the framework and the design system, so you never have to work out which package an export lives in. The seller panel exposes slots on its product, order, payout, profile, and team pages — seller.product.form_sidebar, seller.order.form_sidebar, seller.payout.form_sidebar, seller.profile.form_main and seller.profile.form_sidebar, plus seller.team.actions and seller.team.after. The operator’s side has its own: seller.form_main and seller.form_sidebar on the admin dashboard’s seller page, and seller_payout.form_main and seller_payout.form_sidebar on its payout page. See the slots catalog for each one’s context. To put a widget anywhere else in the panel, add a route.
White-labelling is theming and copy, not a branding config object. There is no logo or brand-name setting to fill in. You restyle by owning the Tailwind layer in your host app, and you re-word by overriding translation keys. Both are real customization paths — there is simply no shortcut that skips them.
@spree/seller-dashboard ships English only. The admin dashboard ships several locales; the seller panel ships en.json and nothing else. A marketplace serving sellers in another language supplies that locale itself, through i18n.addResourceBundle in the host app.
Read more: Dashboard overview for the package split and both customization paths, Dashboard customization for the registries in detail, and Plugins for shipping a redistributable one.

Build against the Seller API

If you are not using the shipped panel — a native seller app, an ERP integration, a bespoke portal — the Seller API is the surface, and @spree/seller-sdk is the client. Every request is scoped to the seller making it. No endpoint takes a seller ID. That is the security property worth relying on: a seller cannot ask for another seller’s data, because there is nowhere in the request to name one.
sellerClient.me is an object with get() and update(). Code written against @spree/seller-sdk 1.0.0-beta.1 or beta.2 that calls sellerClient.me() directly keeps working, but that form is deprecated and logs a one-time warning — switch to sellerClient.me.get().
Authentication is JWT only. There is deliberately no secret-key equivalent: a key that acts as a seller without a seller signing in is exactly what the Seller API’s design exists to prevent. Sign-in fails for a store staff member who runs no seller, even though staff share the same user class. The chosen seller travels as X-Spree-Seller-Id. The store is derived from the seller server-side and never sent alongside, so no header a client can set widens what it reaches.

What the client covers

Two asymmetries are worth noticing, because both are deliberate:
  • A seller cannot set a product’s status. They call submit, and the marketplace decides. Bulk moves exist only for the transitions a seller may make alone.
  • A seller cannot mark an order delivered. That a parcel arrived is the buyer’s word, not the sender’s, so confirming receipt stays with the operator and the carrier feed.
What a seller’s staff may do within all this is governed by roles the seller owns — the same permission system as the back office, with a narrower set of keys, so a seller role can never reach store settings. Read more: Seller API introduction, Seller API authentication, Seller API errors.