Skip to main content

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

See Querying for the full list of filtering, sorting, and pagination options.

Getting a Product

Pass 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:
For something with no options at all, send prices directly and skip the variants array — Spree forwards them to the product’s single variant:
Don’t pass both. Update, clone, or archive a product (deleting soft-deletes it):
Operating on many products at once? The Admin API has bulk actions — bulkStatusUpdate, bulkAddToCategories, bulkAddTags, bulkDestroy, and more. See the Admin API endpoint index.

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 only active 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 as withdrawn, 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 a Spree::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.
Never store a rejection reason on the product itself. A seller can write their own product’s metadata, so a note kept there is erased the next time they save.
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 in proposed 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.
For a single-variant product that’s the only variant. For a product with options it’s the first one, unless you say otherwise. If the default is ever removed, another is promoted automatically — a product is never left without one. Products can also carry 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:
Options are named by value rather than by ID, so you don’t have to look up an option value before creating a variant that uses it.

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.
Create option types (and their values) via the Admin API. Sending 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.
Editing a product type never rewrites existing products. Add an option type to Footwear next month and the shoes you created last month are untouched.
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:
It runs in the background and is additive like the rest — never a side effect of saving a type.
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.
A type in use can’t be deleted; its products would lose the structure they were built from.

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 a thumbnail_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:
Avoid using ?expand=media on listing pages. This loads all images for every product in the response, which is unnecessary when you only need a thumbnail. Use thumbnail_url instead and only expand full media on the product detail page.

All Images

On the product detail page, expand media 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.
Most digital products are both: a digital variant that carries the files it delivers. But the two are decoupled on purpose, so “ships nothing” and “hands over a file” can be mixed as a merchant needs.

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:
Digital files must be uploaded to private storage. The Admin API refuses a blob that landed on the public service — attaching never moves a file between services, so a public upload would stay publicly readable while looking attached. Request the direct upload with private: true.
Replacing an asset’s file keeps every download link that was already issued working: links resolve through the asset, not the underlying blob, so a merchant can swap a corrected file mid‑sale without breaking anyone’s access. 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::DigitalAssetMailer sends 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 a GET 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:
  1. The grant must be live — attempts remaining, and not past its expiry.
  2. The signed‑URL window must be open — clamped to whatever is shorter, the store’s link lifetime or the link’s own remaining days.
  3. The deliverable is produced — see providers below. This step is allowed to fail.
  4. The download is charged — the counter is incremented under a lock.
  5. The file is handed over — a redirect to a signed URL, or an inline body.
The ordering is deliberate: producing the deliverable comes before charging the click, so a provider outage or a missing file returns an honest error and costs the customer nothing. Two allowances govern access, both falling back to store settings when the asset leaves them blank: 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 blank provider_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.
To build one, see the Build a Custom Digital Asset Provider how‑to. It covers the #deliver contract, per‑asset settings, and registration.

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.
Collections work the same way from a storefront’s point of view:
A rule-based collection is defined once and maintains itself — set it to match everything tagged 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 a ProductPublication 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 under product_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 a product_publications array. The array represents the complete desired state; channels absent from the payload are detached.
  • Per-channel, bulk — POST /api/v3/admin/channels/{id}/add_products and POST /api/v3/admin/channels/{id}/remove_products for publishing or unpublishing many products at once. Idempotent: re-publishing an already-published product is a no-op for its window unless published_at / unpublished_at are explicitly passed.
The two surfaces converge on the same spree_product_publications table — pick whichever matches your call site.

Listing products on a specific channel

Storefronts and client.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:
For Admin API filtering across channels (back-office reports, admin UI lists), use Ransack instead: 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 where default = 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.