Overview
A product is the listing a customer browses. A variant is the thing they actually buy. That split runs through everything on this page. The product holds what’s shared — name, description, images, which categories it’s filed under. Each variant holds what differs — its SKU, its price, its stock. A T-shirt is one product; small navy is a variant. Every product has at least one variant, even when there’s nothing to choose. A book with no size or colour still has a single variant carrying its SKU, price and stock; you just never render a picker.Product names, descriptions, slugs and SEO fields are translatable.
Product Attributes
The Admin API adds the operational fields on top:
product_type_id, delivery_profile_id, tax_category_id, seller_id, metadata, created_at / updated_at / deleted_at.
Listing Products
Getting a Product
expand to include related resources in a single response — see expand relations for how relation inclusion works.
Managing Products
The examples above use the Store API (publishable key, read-only, customer-facing). To create and manage products, use the Admin API — via the Admin SDK or the Spree CLI. A product’s purchasable attributes (SKU, prices, stock) live on its variants, which you can create inline. For a product with options, send the variants:Seller submissions
On a marketplace, a seller lists a product but does not publish one. They submit it, and the marketplace decides. This adds two statuses to the three above — both hidden from the storefront, since onlyactive is visible.
This applies to products that belong to a seller. A marketplace’s own catalog is unaffected: an operator publishing their own product sets
status directly and answers to nobody.The lifecycle
Withdrawing a submission before anyone has ruled on it closes the open row aswithdrawn, so a pending row always means the marketplace still owes an answer. A product that was already rejected keeps that decision at the head of its trail instead.
A seller can always take their own listing down — that is not a review decision. Putting one up is.
The submission record
Each submission and each decision on it is aSpree::ProductSubmission row. The product’s status stays the operational truth; these rows are how it got there.
Rows accumulate rather than overwrite, so a seller sent back three times leaves three rows. The latest row for a product is the live one; the ones before it are the trail.
An approval with no
reviewed_by_id and metadata.auto_approved set means the store approves listings automatically — never a decision whose author was lost. Turn that on with the auto_approve_seller_products store preference.
Submitting, as a seller
Status is not writable on the seller branch. A seller moves a product with an explicit action:Deciding, as the marketplace
The seller sees the note and when the decision was made, but never who made it.
Leaving review is a decision
A product inproposed or rejected cannot have its status changed by an ordinary update — that would put it on sale with nobody’s name against it. The refusal lives in the product update workflow, so every caller inherits it, and bulk status updates skip those products and report how many they left behind.
Events
Each transition publishes an event you can subscribe to:
The submission row itself also publishes
product_submission.created and product_submission.updated, carrying the status, the note and the product. See Events.
Product Filters
Get available filter options for building a faceted search UI. Returns price ranges, option values, and categories with counts:Variants
A product is not the thing you buy — a variant is. The product is the listing; the variant is the actual item with a SKU, a price and stock. That distinction is worth holding onto, because everything purchasable lives on the variant:
Variants also carry the customs attributes —
hs_code, country_of_origin, customs_description — used when a parcel crosses a border. See Fees.
Every product has at least one variant
There’s no such thing as a product without one. A book with no size or colour still has a single variant holding its SKU, price and stock — you simply never show a picker for it.Earlier versions of Spree had a special “master variant” alongside the real ones, which meant every query had to remember to exclude it. That concept is gone. A product’s variants are all real, all purchasable, and all the same kind of thing.
The default variant
default_variant_id names the variant that represents the product — the price shown on a listing page, and what “add to cart” means before anyone picks anything.
buy_box_variant_id, naming the variant a marketplace has chosen to feature when several sellers offer the same listing.
Options make variants
A product with option types has one variant per combination. A T-shirt in three sizes and two colours is six variants:
Adding one to an existing product:
Option Types and Option Values
Option types define the axes of variation for a product (e.g., Size, Color, Material). Option values are the specific choices within each type (e.g., Small, Medium, Large). A product must have at least one option type to have multiple variants. Option types and their values are included in the product response when requested:Option type
name and label fields are translatable.option_values replaces the full set, so include every value you want to keep:
Product Types
Merchants who sell more than one kind of thing end up repeating themselves. Every pair of shoes needs Size and Colour, belongs under Footwear, and wants a Material field. Every book needs an ISBN and an author. A product type captures that once. Creating a product from a type gives it the right option types, the right categories, the right delivery profile, and a form asking for the fields that kind of product actually needs.A type is a template, not a controller
This is the part that determines how you should think about them. That’s deliberate. A merchant who adds a field to a type is describing what new products should look like — not asking Spree to silently restructure a live catalogue, invalidate URLs, or change what customers can pick. So the pieces behave in two distinct ways:
Seeding is also additive — it adds what’s missing and never removes what a product already has. Reassigning a product’s type is therefore safe: it seeds the new type’s option types and categories alongside whatever was already there.
If you do want an edited type to reach the products already using it, that’s an explicit action:
required on a type’s custom field is advisory — it marks the field in the dashboard but isn’t enforced on write, since Spree saves the product and its fields in two steps. Validate in your own tooling if you need it enforced.Media
Media can be attached to a product or to individual variants. When displaying a product, show the images for the selected variant, falling back to the product’s own.Thumbnails
Every product response includes athumbnail_url field — the URL to the first image, ready to use without any expands. Similarly, each variant includes a thumbnail_url URL and an media_count counter.
Use these fields for product listing pages to avoid loading all images:
All Images
On the product detail page, expandmedia and variants to get the full set of images. Images are ordered by position:
Prices
Each variant can have multiple prices — one per currency, plus additional prices from Price Lists that apply conditionally based on market, geography, customer segment, or quantity. The API automatically returns the correct price based on the current currency and market context:
See the Pricing guide for details on Price Lists, Price Rules, and market-specific pricing.
Digital products
A digital product is an ordinary product whose variant delivers without shipping — its delivery profile is a digital one, so buying it grants the customer a download instead of dispatching a parcel. Nothing about the catalog model changes — you still have a product, its variants, and its prices. What that variant hands over is usually a digital asset it carries (an e‑book, a design file) or a value a provider mints on demand (a license key) — but the file is the optional deliverable, not what makes the product digital. Two ideas are worth separating up front, because they are independent:- Being digital is a delivery decision. A variant is digital when its delivery profile is a digital one — that variant needs no shipping address, and a cart made up entirely of digital variants skips the delivery step at checkout.
- Carrying downloadable files is a catalog decision. Any variant can own digital assets — including a physical one, so a boxed product can ship a warranty PDF or a setup guide alongside the goods.
Assets live on the variant
A digital asset (Spree::DigitalAsset) belongs to a variant and holds either an uploaded file or a reference to a provider that produces the deliverable on demand (see Where the file comes from below). Uploaded files go to private storage — they are only ever served through a short‑lived, signed link, never a public URL.
You attach assets to a variant from the product’s Digital files card in the dashboard, or through the Admin API nested under the product:
A purchase grants download links
When an order is placed, digital assets fulfill themselves. The digital fulfillment provider runs automatically (it needs no address and no manual action) and, for each purchased unit, creates one download link (Spree::DigitalLink) per asset on the variant. Buy three copies of a two‑file bundle and the buyer gets six links. Re‑running fulfillment is idempotent — it never duplicates links.
A download link is the customer’s grant: a globally unique token, a counter of downloads spent, and its own copy of the allowance. The token is the credential — it identifies the store on its own, which is why an emailed link works without any API key.
Customers reach their files two ways:
- By email. On order placement,
Spree::DigitalAssetMailersends a “your files are ready” message with the links — a message of its own, separate from the order confirmation, so it can be re‑sent from the order page later. - From their account. A signed‑in customer sees every link they have ever been granted, across all their orders, through the customer downloads endpoint.
The files-ready email is only sent when the order actually has digital links and the store has consumer transactional emails enabled. A store that delivers files through its own storefront or webhooks can leave it off.
Downloading, and the allowance
A download is aGET against the link’s token. Every download runs the same guarded sequence, and the customer’s allowance is only spent once a deliverable is actually in hand:
- The grant must be live — attempts remaining, and not past its expiry.
- The signed‑URL window must be open — clamped to whatever is shorter, the store’s link lifetime or the link’s own remaining days.
- The deliverable is produced — see providers below. This step is allowed to fail.
- The download is charged — the counter is incremented under a lock.
- The file is handed over — a redirect to a signed URL, or an inline body.
Store‑wide defaults and their on/off switches live at Settings → Store; the separate
digital_asset_link_expire_time preference caps how long a single signed URL lives (default 5 minutes, never more than an hour) because that URL is a bearer credential. When a customer runs out of downloads or a file was replaced mid‑flight, an admin can restore access by resetting the link from the order page, which zeroes the counter and restarts the clock.
Each successful download publishes an event you can subscribe to:
Where the file comes from
By default a digital asset delivers its uploaded file. But the last step — “hand something over” — is pluggable through a digital asset provider, so the deliverable can instead be minted on demand: a license key from your billing system, an entitlement from internal software, or a signed link to a file on your own host. The purchase, the grant, the allowance, and the email are identical either way; only the production of the deliverable changes. A blankprovider_type on an asset means the built‑in file provider — the uploaded‑file behavior described above. Registering your own provider adds it as a source on the Digital files card, so a merchant can pick it when adding an asset.
Categories
There are two ways to group products, and they answer different questions. Categories are a hierarchy — the navigation tree a shopper browses. Clothing contains T-Shirts, which contains Long Sleeve. A product can sit in several categories, and each one has a permalink built from its path. Collections are flat groupings — “Summer 2025”, “Best Sellers”, “Under $50”. A collection can be curated by hand, or defined by rules so products join and leave it on their own as their price, tags or stock change.
A brand is usually best modelled as one or the other rather than as a separate concept — a category if you want it in the navigation tree, a collection if it’s a landing page.
Category
name and description fields are translatable.sale and under $50, and products appear and disappear as those facts change. Ordering can be manual or by a rule such as newest first.
Publications and Sales Channels
A product is visible on a Channel only when aProductPublication record joins the two. Publications carry an optional time window so a product can be scheduled to go live and come down without code or manual toggles.
Product
status (draft / active / archived) is the outer gate: a Draft or Archived product is hidden on every channel regardless of its publication window. Only active products consult publication state.
Reading publications
Publications appear in the API underproduct_publications when expanded; the same data is available through the channels association as a flat list of joined channels.
Response
Writing publications
Two write surfaces serve different shapes:-
Per-product, full-set —
PATCH /api/v3/admin/products/{id}with aproduct_publicationsarray. The array represents the complete desired state; channels absent from the payload are detached. -
Per-channel, bulk —
POST /api/v3/admin/channels/{id}/add_productsandPOST /api/v3/admin/channels/{id}/remove_productsfor publishing or unpublishing many products at once. Idempotent: re-publishing an already-published product is a no-op for its window unlesspublished_at/unpublished_atare explicitly passed.
spree_product_publications table — pick whichever matches your call site.
Listing products on a specific channel
Storefronts andclient.products.list() calls return only products published on the resolved channel (live within the publication window, with the product itself active). To scope a Store SDK request to a non-default channel — e.g. a POS app querying for the POS catalog — set the channel code on the client or per-request:
q[channels_id_in][]=ch_xxx. See Sales Channels for the resolution rules.
Auto-publish on the default channel
When a product is created via the dashboard, it is auto-published on the store’s default channel (the only channel wheredefault = true). The Admin API does not auto-publish — supply product_publications: [{ channel_id }] on create or call add_products afterwards.
See Sales Channels for the full channel lifecycle, including default-channel resolution and the X-Spree-Channel header.
Related Documentation
- Sales Channels — Channels, publications, and order attribution
- Pricing — Price Lists, Price Rules, and market-specific pricing
- Inventory — Stock management and backorders
- Media — Image management
- Build a Custom Digital Asset Provider — Deliver a license key or external file instead of an uploaded one
- Translations — Translating product content
- Search & Filtering — Full-text search and Ransack filtering
- Store SDK Products — Listing, fetching, filtering, and categories via
client.products - Querying — API filtering, sorting, and pagination

