What you get
- Companies — buying organizations as a tree of companies and divisions, with members, invitations, a shared address book, and tax registrations on the legal entities.
- Customer groups — segments of individual customers, such as approved trade accounts or staff, for buyers who do not purchase on behalf of a company.
- Catalogs — the commercial agreement: which products an audience sees, what they pay, and how much they must order.
- Negotiated and volume pricing — fixed prices per product, a percentage off the shop price, and quantity breaks, all resolved per buyer.
- Order terms — minimum order quantities, order multiples, and a minimum order value in each currency.
- Gated access — a sales channel that hides prices from guests or refuses them entirely, with guest checkout switched off.
- B2B checkout — the company a purchase is for, its address book, its tax treatment, and the buyer’s own purchase order number.
- Orders on account — pay-by-invoice at checkout, and staff-keyed draft orders with negotiated line prices, placed with payment still to come.
- Freight — carton and pallet packing, volume-based shipment tiers, and delivery rates that are quoted after review.
Before you start
You need a running Spree 6 project and admin credentials. B2B trading uses the same surfaces as the rest of Spree:
Every new store is seeded with the basics of a wholesale setup:
- a Wholesale sales channel (code
wholesale) that requires sign-in and does not allow guest checkout - a Wholesale customer group
- a publishable API key bound to the wholesale channel
wholesale@example.com / spree123. The sample data also creates a Wholesale Assortment catalog assigned to Acme Industrial. It is created switched off, so activate it when you want to see a restricted range.
Decide who your buyers are
Spree has two ways to describe a trade buyer. Pick the one that matches how your customers actually buy, because a catalog is assigned to one or the other.Companies
A person buying on behalf of an organization. The organization has several buyers, several delivery sites, a VAT number, and a finance team that wants to see everything anyone ordered.Use companies for distributors, retailers, contractors and any account with a negotiated deal.
Customer groups
An individual in a segment. A person who qualifies for trade pricing but buys for themselves, such as a sole trader, a professional, a member of staff or a loyalty tier.Use customer groups when there is no organization behind the buyer.
Model the buying organizations
A company is a tree. The root is always a legal entity, and beneath it sit subsidiaries and divisions, each with their own people and addresses. Trees are capped at five levels.Create the tree
parent_id. Deleting a node removes its whole subtree, and Spree refuses to delete it once any order exists beneath it.
A delivery site is an address, not a node. Ten warehouses do not mean ten companies. Nodes describe the organization and its legal structure, and addresses describe where goods go.
Add the buyers
People join a company by email. When staff add someone through the Admin API, an existing customer becomes a member immediately, and an unknown email receives an invitation. The ID prefix tells you which happened:cmem_ for a membership, cinv_ for an invitation.
When a member adds a colleague from the storefront, the colleague always receives an invitation, even if they already have an account. Anyone can type an email address, so joining requires accepting the emailed invitation, which proves the person owns that address. An invitation is valid for 30 days. Accepting it creates the account and the membership in one step, or links an existing signed-in customer whose email matches.
Fill the address book
A company keeps its own labelled addresses, separate from any individual’s. One ship-to address and one bill-to address can be the default. At checkout, buyers choose from this book instead of typing an address.Segment individual buyers with customer groups
For buyers who are not purchasing for a company, a customer group is the audience. The seeded Wholesale group is where approved trade accounts land, and the wholesale portal treats membership of it as approval.Write the commercial agreement
A catalog is the agreement with an audience. It answers every commercial question in one place:The two ways a catalog is used
Whether the assortment is empty decides how the catalog behaves:Pricing overlay
Assortment empty. The audience browses your whole range, but at the catalog’s prices and on its terms.Use this for “Acme sees everything, just at their negotiated prices”.
Restricted range
Assortment has products. The audience sees only those products.Use this for a wholesale-only range, or a distributor who may only sell certain lines.
Create one
A catalog can be created in a single request with its audience, pricing and terms. Catalogs are always created switched off, so nobody sees a half-finished agreement.What a buyer ends up with
1
Find the catalogs that apply
For a company buyer, that means the catalogs on their node and every node above it. For anyone else, their customer groups’ catalogs. If neither applies, the channel’s default catalog, if it has one.
2
Combine the assortments
The buyer sees everything those catalogs contain, so a division’s extra catalog adds to the group’s range rather than replacing it. If any one of them is a pricing overlay, nothing is hidden.
3
Price each line
The catalogs’ price lists are checked nearest first, so a subsidiary’s own agreement beats the group’s. Then ordinary price lists whose rules match. Then the product’s base price.
Set negotiated and volume pricing
A catalog prices through a price list it owns. That list is configured together with the catalog, is never matched by rules of its own, and cannot leak to other shoppers. It supports several ways of stating a price, and they combine.- Quantity is measured per line. Ten of one SKU reaches a ten-unit break. Five each of two SKUs does not.
- A variant with quantity breaks is priced by its breaks alone. The catalog’s percentage does not apply on top of them.
- A break may never cost more than the quantity below it. Saving is refused, so a buyer never pays more for ordering more. A variant can carry up to ten breaks per currency on one list.
- Prices are per currency. Spree never converts a negotiated price, so state the agreement in every currency the buyer trades in.
The Customer Group and User price rules still work on price lists that already use them, but they are no longer offered for new ones. To target who gets a price, assign a catalog to them.
Set how much a buyer must order
Wholesale goods are rarely sold one at a time. Two rules govern each line, and one governs the whole order.
A minimum of 48 with a multiple of 24 allows 48, 72 and 96, and refuses 50. Steps count from a stated minimum, so “at least 50, in 24s” allows 50, 74 and 98.
Where the terms are set
The quantity rules resolve through three levels. For each rule, the most specific level that states it wins:- A per-product override on a catalog
- The catalog-wide default
- The variant’s own rule, which applies to every buyer, including retail
adminClient.catalogs.products.list('cat_xxx', { expand: ['quantity_rule'] }).
How the storefront sees them
The Store API returns each variant’s resolvedminimum_order_quantity and order_multiple for the signed-in buyer, so a storefront can draw a quantity stepper without knowing where the rule came from. An unrestricted variant reads 1 and 1.
The cart carries order_minimum, order_minimum_shortfall and below_order_minimum, which is enough to show “€180 to go” without any arithmetic. These fields are hidden from guests on a channel that hides prices.
Spree enforces the terms on the server:
- Adding to the cart refuses a quantity that breaks a rule, and the error names the nearest valid quantities. Spree never rounds a quantity silently.
- Completing checkout checks every line again, because an agreement can change while a cart is open. A failing line reports
quantity_rule_violated. - An order below its minimum shows an
order_minimum_not_metrequirement on the cart and cannot be completed.
Stored quantities are always units. If a product is sold by the carton, set
purchase_unit: 'carton' and units_per_carton on the variant so the storefront can present “2 cartons” while the order holds 96 units. See how products pack.Gate the storefront
Channels decide where a buyer is shopping. A wholesale channel is an ordinary sales channel with a stricter posture towards visitors who have not signed in.
The gate is enforced by the Store API, not by the storefront, so a storefront app cannot loosen it. Signed-in customers are never gated. A companion setting, guest checkout, decides whether an order can be placed without an account.
- the
X-Spree-Channel: wholesaleheader, which the Store SDK sends when created withchannel: 'wholesale' - a publishable key bound to the channel, like the one seeded for every store
Run the wholesale portal
The Next.js storefront ships an optional wholesale portal at/wholesale: a gated trade surface running beside your public store, from the same deployment. It is off until you point it at a channel.
storefront/.env.local
X-Spree-Channel on every request, keeps a separate cart for the wholesale surface, and shares the customer’s sign-in with the public store. It supports both gated modes:
- login_required
A guest sees a sign-in and apply wall instead of every wholesale page. The Store API refuses their reads, so nothing about the trade catalog or its prices leaks.
The API gates guests. Which signed-in customers count as approved is the portal’s decision, made from the
customer_groups on the customer’s profile. Pricing needs no extra guard: a customer outside every trade audience resolves no trade catalog and pays shop prices.Check out for a company
A cart that names a company becomes a company purchase. That one field decides which catalog prices it, which address book is offered, whose tax registration applies, and who else will see the order.Purchase order numbers
For a corporate buyer, their own purchase order number matters as much as your order number. Their accounting reconciles the order, the invoice and the payment against it.- The cart and the order carry
po_number. It shows in order search, on the order, and in the confirmation email. - Turn on
po_number_requiredon a company and its buyers must supply a number before they can complete checkout. The cart reportspo_number_required: trueand apo_number_requiredrequirement until they do. - A buyer can also attach the signed PO document itself: a PDF, an image or a Word file of up to 10 MB, stored privately.
adminClient.orders.list({ po_number_eq: 'PO-2026-0418' }), and download the document from GET /api/v3/admin/orders/:id/po_document. Staff keying in an order are not asked for a PO number, because the reference often arrives with the paperwork later. It can be corrected on a placed order at any time.
What a finance team sees
Take orders on account
Trade buyers often do not pay by card at checkout. Spree supports two ways to take an order now and collect the money later.The buyer checks out on account
An offline payment method such as Pay by invoice or Bank transfer. The order is placed at checkout, and staff capture the payment when the money arrives.
Staff key in the order
A draft order built by your sales team, with negotiated line prices and the buyer’s PO. It is placed with payment still pending.
Pay by invoice at checkout
Spree’s built-in Check payment method records a payment without contacting any provider. Create one from Settings → Payment methods and name it for your terms, such as Pay by invoice (30 days). At completion the payment succeeds immediately and waits aspending. Staff capture it from the order once the bank transfer lands. See the direct payment flow.
Every active payment method that is visible on the storefront is offered on every order. To offer invoice terms only to company buyers, subclass the method and narrow where it is available:
server/app/models/spree/payment_method/invoice.rb
server/config/initializers/spree.rb
Staff-keyed draft orders
When an order arrives by email or phone, or after a negotiation, your team builds it as a draft. A draft can name the company, carry the buyer’s PO, and set a negotiated unit price on any line.company_id when creating or updating a draft makes it a company purchase, with that company’s catalog prices and tax treatment. Negotiated prices can only be set before the order is placed. Send price: null on a line to return it to catalog pricing. After placement, change money through fees and discounts instead.
Completing with payment_pending: true places the order without processing payments. It gets its number, stock is allocated and the order.placed event fires, while payment_status stays none. When the invoice is paid, record the payment against the order and capture it:
Admin SDK
Payment terms such as net 30, credit limits and deposits are not part of open-source Spree. Partial payments are available. See Payments.
Handle business tax
Tax registrations and exemption certificates live on legal entities. A purchase resolves tax through the nearestcompany node at or above the one it is for. A division uses its parent company’s registration.
Registrations
Exemption certificates
A certificate startspending and exempts nothing until staff verify it. Scope it to the country or state that issued it, or leave both empty for one that holds everywhere.
active rather than its status: a verified certificate stops counting once it expires. A verified certificate cannot be deleted, so revoke it instead.
Spree’s built-in tax rates apply exemption certificates. EU reverse charge on business sales needs a tax service that supports it. The built-in rates declare that they do not, so the dashboard can warn a merchant who pairs them with a market that needs it.
Ship by the carton and pallet
A wholesale order leaves as cartons, a pallet or a container, and international freight is usually quoted by a forwarder after someone looks at the order. Spree handles this alongside retail parcel shipping, on the same products.1
Say how each product packs
Create a package type of kind
carton in Settings → Package types. Then record on each variant how many units one carton holds, what a packed carton weighs, and how many cartons stack on a pallet. The chain runs from units to cartons, to pallets, to cubic meters and weight.2
Read the load
Carts and orders carry a freight summary: total units, cartons, pallets, cubic meters and weight. When
complete is false, part of the catalog has no carton data, so treat the figures as a minimum.3
Define the shipment tiers
Each tier, such as Cartons, Pallet or 20ft container, is an ordinary delivery method. A volume rule bounds it by cubic meters. A company rule offers it only to company purchases, which keeps freight away from retail carts and parcel methods away from trade buyers.
4
Quote after review
A method priced by the Freight rate provider returns an unpriced rate. It reads Quoted after review rather than Free, is sorted after every priced option, and is never preselected. Checkout completes without a shipping price.
Run it from the dashboard
Everything in this guide can also be managed in the admin dashboard, without the API.
To add your own B2B cards, such as a credit account or an ERP reference, the company page has two slots:
company.form_main and company.form_sidebar.
Extend it
The B2B building blocks are ordinary Spree resources, so the usual customization paths apply:- Events. Companies, company invitations and catalogs publish lifecycle events, and orders publish
order.placed. Subscribe to them to sync accounts and orders with an ERP or CRM. See Events. - External references. Companies accept
external_references, so an ERP can address a company by its own account number. - Tax identifier validators. Register a validator that checks numbers against a live registry.
- Checkout requirements. Register an extra requirement, such as “a cost center is required”, and it appears in the cart’s
requirementsnext to the built-in ones. See Checkout customization.
What Spree Enterprise adds
Open-source Spree trusts every member of a company equally. Each member can buy, see the subtree’s orders, and manage addresses and colleagues. That is the right default for a company of five people, but not for one of five hundred.Company governance requires a Spree Enterprise licence
Company roles built from capabilities (place orders, approve orders, view purchases, manage members and addresses), order approvals with an
approval_required checkout response, spending limits per member or per node, and governance audit history. They work through the same endpoints and data, so a storefront built against open source keeps working when governance is switched on.Related
- Companies — the tree, memberships, address book and tax anchoring
- Catalogs — assortments, audiences and how pricing resolves
- Pricing — price lists, rules and the pricing context
- Channels — sales channels and storefront access gating
- Taxes — tax identifiers, exemptions and tax services
- Freight — cartons, pallets and quoted freight
- Fees — surcharges and handling charges
- Payments — offline methods and partial payments
- Wholesale Portal — the Next.js trade surface
- Sell to Businesses (operator guide) — the same setup from the dashboard
- Catalogs (operator guide) — the catalog wizard and order terms
- Price Lists (operator guide) — quantity breaks and CSV import

