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:
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.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.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 fromtypes() 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.
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.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.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.
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
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 throughseller_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’sstatus 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”.
Both sides of the review
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.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
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.
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 withcreate-spree-app schedules both already:
server/config/recurring.yml
Settling by hand
A seller on themanual 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 inSpree.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:/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:
.env.local holds one setting — the Rails server the dev proxy forwards /api to:
apps/seller-dashboard/.env.local
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
@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.
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().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.
Related
- Sellers — the full model: lifecycle, ownership, order splitting, ledger
- Commissions — rates, rules, precedence, currency handling, commission tax
- Products — the listing review flow in detail
- Orders — orders and their statuses
- Customers — who buys, across every seller
- Delivery setup — how a seller’s goods ship and what they ship in
- Staff & Roles — how seller teams are governed
- Seller payouts — writing your own payout provider
- Stripe Connect — the shipped payout provider
- Seller API — the full endpoint reference
- Dashboard overview — customizing either panel
- Sellers (operator guide) — running a marketplace from the dashboard
- Seller requirements (operator guide) — building the checklist
- Payouts settings (operator guide) — who pays sellers and how often

