Skip to main content

Overview

A product represents something you sell. Each product has one or more variants — the actual purchasable items with their own SKU, price, and inventory. For example, a “T-Shirt” product might have variants for each size and color combination. Products are organized into categories — a flexible hierarchy for grouping products. Categories can be filtered, sorted, and searched via the Store API.
Product names, descriptions, slugs, and SEO fields are translatable.

Product Attributes

AttributeDescriptionTranslatable
nameProduct nameYes
descriptionFull product descriptionYes
slugURL-friendly identifier (e.g., spree-tote)Yes
statusdraft, active, or archivedNo
available_onDate the product becomes available for saleNo
discontinue_onDate the product is no longer availableNo
meta_titleCustom SEO titleYes
meta_descriptionSEO descriptionYes
meta_keywordsSEO keywordsYes
purchasableWhether the product can be added to cartNo
in_stockWhether any variant has stock availableNo
priceDefault variant’s price in the current currencyNo
thumbnail_urlURL to the product’s first image — always returned, no expand neededNo
tagsArray of tag strings for filteringNo

Listing Products

// List products with pagination
const { data: products, meta } = await client.products.list({
  limit: 12,
  page: 1,
})

// Filter by price range and availability
const filtered = await client.products.list({
  price_gte: 10,
  price_lte: 50,
  in_stock: true,
})

// Search by keyword
const results = await client.products.list({
  search: 'tote bag',
})

// Sort products
const sorted = await client.products.list({
  sort: 'price_high_to_low',  // or: price_low_to_high, newest, name_a_z, name_z_a
})
See Querying for the full list of filtering, sorting, and pagination options.

Getting a Product

// Get by slug
const product = await client.products.get('spree-tote')

// Get with included relations
const detailed = await client.products.get('spree-tote', {
  expand: ['variants', 'media', 'option_types', 'categories'],
})
// detailed.variants => [{ id: "var_xxx", sku: "TOTE-S-R", price: { amount: "15.99", currency: "USD" }, ... }]
// detailed.media => [{ id: "img_xxx", url: "https://cdn...", position: 1 }]
// detailed.option_types => [{ name: "size", presentation: "Size", option_values: [...] }]

Product Filters

Get available filter options for building a faceted search UI. Returns price ranges, option values, and categories with counts:
const filters = await client.products.filters()
// {
//   option_types: [{ name: "size", option_values: [{ name: "Small", count: 12 }, ...] }],
//   price_range: { min: 9.99, max: 199.99 },
//   categories: [{ id: "ctg_xxx", name: "Clothing", count: 45 }],
// }

// Scoped to a specific category
const categoryFilters = await client.products.filters({
  category_id: 'ctg_xxx',
})

Variants

Variants are the purchasable units of a product. Each variant has its own SKU, price, inventory, and images, and is defined by a unique combination of option values.
AttributeDescription
skuUnique stock keeping unit
barcodeBarcode (UPC, EAN, etc.)
pricePrice in the current currency
original_priceCompare-at price for showing discounts
weight, height, width, depthDimensions for shipping calculations
in_stockWhether stock is available
backorderableWhether the variant can be ordered when out of stock
option_valuesThe option values that define this variant (e.g., Size: Small, Color: Red)

Master Variant

Every product has a master variant that holds default pricing and inventory. If a product has no option types (e.g., a book with no size/color), the master variant is the only purchasable variant.

Regular Variants

When a product has option types, each unique combination of option values creates a variant. For example, a T-shirt with sizes (S, M, L) and colors (Red, Green) has 6 variants:
SKUSizeColor
TEE-S-RSmallRed
TEE-S-GSmallGreen
TEE-M-RMediumRed
TEE-M-GMediumGreen
TEE-L-RLargeRed
TEE-L-GLargeGreen
The product’s default_variant_id points to the first non-master variant (or the master variant if none exist).

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:
const product = await client.products.get('spree-tee', {
  expand: ['option_types'],
})

product.option_types?.forEach(optionType => {
  console.log(optionType.presentation) // "Size"
  optionType.option_values.forEach(value => {
    console.log(value.presentation)    // "Small", "Medium", "Large"
  })
})
Option type name and presentation fields are translatable.

Media

Media can be attached to the product (via the master variant) or to individual variants. When displaying a product, show the images for the selected variant, falling back to the product-level images.

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:
// List products — thumbnail_url is always included
const { data: products } = await client.products.list({ limit: 12 })

products.forEach(product => {
  product.thumbnail_url // "https://cdn.../tote-front.jpg" — no expand needed
})
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:
const product = await client.products.get('spree-tote', {
  expand: ['media', 'variants'],
})

// Product-level images (from master variant)
product.media // [{ url: "https://cdn.../tote-front.jpg", position: 1 }, ...]

// Each variant has its own thumbnail and media_count
product.variants?.forEach(variant => {
  variant.thumbnail    // "https://cdn.../tote-red.jpg" — always available
  variant.media_count  // 3 — quick check without loading media
  variant.media        // full image array (only when ?expand=media)
})
FieldAvailable onAlways returnedDescription
thumbnail_urlProductYesURL to the product’s first media
thumbnail_urlVariantYesURL to the variant’s first media
media_countVariantYesNumber of media
mediaProduct, VariantNoFull image array (requires ?expand=media)

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:
FieldDescription
priceCurrent selling price
original_priceCompare-at price (for showing strikethrough discounts)
See the Pricing guide for details on Price Lists, Price Rules, and market-specific pricing.

Categories

Categories provide a flexible way to organize products into hierarchical trees. Internally, Spree uses Taxonomies (category trees) and Taxons (nodes within those trees), but the Store API exposes them simply as Categories. For example:
  • Categories → Clothing → T-Shirts, Dresses
  • Brands → Nike, Adidas, Puma
  • Collections → Summer 2025, Best Sellers
Products can belong to multiple categories.
// List categories
const { data: categories } = await client.categories.list()

// Get a category by permalink
const category = await client.categories.get('clothing/shirts')

// List products in a category
const { data: products } = await client.categories.products.list('clothing/shirts', {
  limit: 12,
})
Category name and description fields are translatable.
  • Pricing — Price Lists, Price Rules, and market-specific pricing
  • Inventory — Stock management and backorders
  • Media — Image management
  • Translations — Translating product content
  • Search & Filtering — Full-text search and Ransack filtering
  • Querying — API filtering, sorting, and pagination