> ## Documentation Index
> Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Products

> How Spree models products, variants, option types, images, prices, and categories — the building blocks of every catalog and storefront.

export const Since = ({version, from}) => {
  const knownPrevious = {
    '5.0': '4.10',
    '6.0': '5.4'
  };
  const previous = (from ?? knownPrevious[version]) ?? (() => {
    const [major, minor] = version.split('.').map(Number);
    if (Number.isNaN(major) || Number.isNaN(minor) || minor < 1) {
      throw new Error(`<Since version="${version}" />: cannot derive previous version automatically. ` + `Pass an explicit "from" prop, e.g. <Since version="${version}" from="X.Y" />.`);
    }
    return `${major}.${minor - 1}`;
  })();
  return <Tooltip tip={`Available since Spree ${version}+.`} cta="Upgrade instructions" href={`/developer/upgrades/${previous}-to-${version}`}>
      <Badge icon="lock">Spree {version}+</Badge>
    </Tooltip>;
};

## 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.

<Info>
  Product names, descriptions, slugs, and SEO fields are [translatable](/developer/core-concepts/translations#resource-translations).
</Info>

```mermaid theme={"theme":"night-owl"}
erDiagram
    Product ||--o{ Variant : "has many"
    Product }o--o{ OptionType : "has many"
    Product ||--o{ Classification : "has many"
    Variant ||--o{ Price : "has many"
    Variant ||--o{ StockItem : "has many"
    Variant ||--o{ Image : "has many"
    Variant }o--o{ OptionValue : "has many"
    OptionType ||--o{ OptionValue : "has many"
    Taxon ||--o{ Classification : "has many"
    Taxonomy ||--o{ Taxon : "has many"

    Product {
        string name
        string slug
        string status
        text description
        datetime available_on
    }

    Variant {
        string sku
        boolean is_master
        decimal weight
    }

    Price {
        decimal amount
        decimal compare_at_amount
        string currency
    }

    OptionType {
        string name
        string presentation
    }

    OptionValue {
        string name
        string presentation
    }
```

## Product Attributes

| Attribute          | Description                                                          | Translatable |
| ------------------ | -------------------------------------------------------------------- | :----------: |
| `name`             | Product name                                                         |      Yes     |
| `description`      | Full product description                                             |      Yes     |
| `slug`             | URL-friendly identifier (e.g., `spree-tote`)                         |      Yes     |
| `status`           | `draft`, `active`, or `archived`                                     |      No      |
| `available_on`     | Date the product becomes available for sale                          |      No      |
| `discontinue_on`   | Date the product is no longer available                              |      No      |
| `meta_title`       | Custom SEO title                                                     |      Yes     |
| `meta_description` | SEO description                                                      |      Yes     |
| `meta_keywords`    | SEO keywords                                                         |      Yes     |
| `purchasable`      | Whether the product can be added to cart                             |      No      |
| `in_stock`         | Whether any variant has stock available                              |      No      |
| `price`            | Default variant's price in the current currency                      |      No      |
| `thumbnail_url`    | URL to the product's first image — always returned, no expand needed |      No      |
| `tags`             | Array of tag strings for filtering                                   |      No      |

## Listing Products

<CodeGroup>
  ```typescript Store SDK theme={"theme":"night-owl"}
  // 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; also: price, name, -name, available_on, -available_on, best_selling
  })
  ```

  ```typescript Admin SDK theme={"theme":"night-owl"}
  const { data: products, meta } = await adminClient.products.list({
    limit: 12,
    page: 1,
  })
  ```

  ```bash cURL theme={"theme":"night-owl"}
  # List products
  curl 'https://api.mystore.com/api/v3/store/products?limit=12&page=1' \
    -H 'X-Spree-API-Key: pk_xxx'

  # Filter by price and stock
  curl 'https://api.mystore.com/api/v3/store/products?q[price_gte]=10&q[price_lte]=50&q[in_stock]=true' \
    -H 'X-Spree-API-Key: pk_xxx'

  # Search
  curl 'https://api.mystore.com/api/v3/store/products?q[search]=tote+bag' \
    -H 'X-Spree-API-Key: pk_xxx'
  ```
</CodeGroup>

See [Querying](/api-reference/store-api/querying) for the full list of filtering, sorting, and pagination options.

## Getting a Product

<CodeGroup>
  ```typescript Store SDK theme={"theme":"night-owl"}
  // 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", original_url: "https://cdn...", position: 1 }]
  // detailed.option_types => [{ name: "size", label: "Size", position: 1, kind: "..." }]
  // detailed.option_values => [{ name: "small", label: "S", option_type_name: "size", ... }]  // separate top-level array when expanded
  ```

  ```typescript Admin SDK theme={"theme":"night-owl"}
  const product = await adminClient.products.get('prod_86Rf07xd4z')
  ```

  ```bash cURL theme={"theme":"night-owl"}
  curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=variants,media,option_types,categories' \
    -H 'X-Spree-API-Key: pk_xxx'
  ```
</CodeGroup>

Pass `expand` to include related resources in a single response — see [expand relations](/api-reference/store-api/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](/api-reference/admin-api/introduction) — via the [Admin SDK](/developer/sdk/admin/quickstart) or the [Spree CLI](/developer/cli/admin-api).

A product's purchasable attributes (SKU, prices, stock) live on its **variants**, which you can create inline:

<CodeGroup>
  ```typescript Admin SDK theme={"theme":"night-owl"}
  import { createAdminClient } from '@spree/admin-sdk'

  const client = createAdminClient({
    baseUrl: 'https://store.example.com',
    secretKey: 'sk_xxx',
  })

  const product = await client.products.create({
    name: 'Premium T-Shirt',
    description: 'Soft, organic cotton.',
    status: 'active',
    variants: [
      {
        sku: 'TSHIRT-S-NAVY',
        options: [
          { name: 'size', value: 'Small' },
          { name: 'color', value: 'navy' },
        ],
        prices: [{ currency: 'USD', amount: '29.99' }],
        stock_items: [{ stock_location_id: 'sloc_xxx', count_on_hand: 50 }],
      },
    ],
  })
  ```

  ```bash CLI theme={"theme":"night-owl"}
  spree api post /products -d '{
    "name": "Premium T-Shirt",
    "status": "active",
    "variants": [{
      "sku": "TSHIRT-S-NAVY",
      "options": [{ "name": "size", "value": "Small" }],
      "prices": [{ "currency": "USD", "amount": "29.99" }]
    }]
  }'
  ```
</CodeGroup>

Update, clone, or archive a product (deleting soft-deletes it):

<CodeGroup>
  ```typescript Admin SDK theme={"theme":"night-owl"}
  await client.products.update('prod_xxx', { name: 'Premium Tee', status: 'active' })
  await client.products.clone('prod_xxx')   // duplicate as a new draft
  await client.products.delete('prod_xxx')  // soft-delete
  ```

  ```bash CLI theme={"theme":"night-owl"}
  spree api patch /products/prod_xxx -d '{"name": "Premium Tee"}'
  spree api post /products/prod_xxx/clone
  spree api delete /products/prod_xxx
  ```
</CodeGroup>

<Tip>
  Operating on many products at once? The Admin API has bulk actions — `bulkStatusUpdate`, `bulkAddToCategories`, `bulkAddTags`, `bulkDestroy`, and more. See the [Admin API endpoint index](/api-reference/admin-api/endpoints).
</Tip>

## Product Filters

Get available filter options for building a faceted search UI. Returns price ranges, option values, and categories with counts:

<CodeGroup>
  ```typescript Store SDK theme={"theme":"night-owl"}
  const filters = await client.products.filters()
  // {
  //   filters: [
  //     { id: "price", type: "price_range", min: 9.99, max: 199.99, currency: "USD" },
  //     { id: "availability", type: "availability", options: [{ id: "in_stock", count: 42 }] },
  //     { id: "opt_xxx", type: "option", name: "size", label: "Size", kind: "...",
  //       options: [{ id: "optv_xxx", name: "small", label: "Small", count: 12 }, ...] },
  //     { id: "categories", type: "category",
  //       options: [{ id: "ctg_xxx", name: "Clothing", permalink: "clothing", count: 45 }] },
  //   ],
  //   sort_options: [{ id: "price" }, ...],
  //   default_sort: "best_selling",
  //   total_count: 120,
  // }

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

  ```bash cURL theme={"theme":"night-owl"}
  curl 'https://api.mystore.com/api/v3/store/products/filters' \
    -H 'X-Spree-API-Key: pk_xxx'

  # Scoped to a category
  curl 'https://api.mystore.com/api/v3/store/products/filters?category_id=ctg_xxx' \
    -H 'X-Spree-API-Key: pk_xxx'
  ```
</CodeGroup>

## 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.

| Attribute                            | Description                                                                |
| ------------------------------------ | -------------------------------------------------------------------------- |
| `sku`                                | Unique stock keeping unit                                                  |
| `barcode`                            | Barcode (UPC, EAN, etc.)                                                   |
| `price`                              | Price in the current currency                                              |
| `original_price`                     | Compare-at price for showing discounts                                     |
| `weight`, `height`, `width`, `depth` | Dimensions for shipping calculations                                       |
| `in_stock`                           | Whether stock is available                                                 |
| `backorderable`                      | Whether the variant can be ordered when out of stock                       |
| `option_values`                      | The 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:

| SKU       | Size   | Color |
| --------- | ------ | ----- |
| `TEE-S-R` | Small  | Red   |
| `TEE-S-G` | Small  | Green |
| `TEE-M-R` | Medium | Red   |
| `TEE-M-G` | Medium | Green |
| `TEE-L-R` | Large  | Red   |
| `TEE-L-G` | Large  | Green |

The product's `default_variant_id` points to the first non-master variant (or the master variant if none exist).

Add a variant to an existing product via the Admin API (SKU, prices, and stock all live on the variant):

<CodeGroup>
  ```typescript Admin SDK theme={"theme":"night-owl"}
  const variant = await client.products.variants.create('prod_xxx', {
    sku: 'TEE-L-R',
    options: [
      { name: 'size', value: 'Large' },
      { name: 'color', value: 'Red' },
    ],
    prices: [{ currency: 'USD', amount: '24.99' }],
    stock_items: [{ stock_location_id: 'sloc_xxx', count_on_hand: 30 }],
  })
  ```

  ```bash CLI theme={"theme":"night-owl"}
  spree api post /products/prod_xxx/variants -d '{
    "sku": "TEE-L-R",
    "options": [{ "name": "size", "value": "Large" }],
    "prices": [{ "currency": "USD", "amount": "24.99" }]
  }'
  ```
</CodeGroup>

## 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:

<CodeGroup>
  ```typescript Store SDK theme={"theme":"night-owl"}
  const product = await client.products.get('spree-tee', {
    expand: ['option_types', 'option_values'],
  })

  // Option types describe the axes of variation
  product.option_types?.forEach(optionType => {
    console.log(optionType.label) // "Size"
  })

  // Option values are a separate flat array; each carries its parent's id/label
  product.option_values?.forEach(value => {
    console.log(value.option_type_label, value.label) // "Size", "Small"
  })
  ```

  ```typescript Admin SDK theme={"theme":"night-owl"}
  const product = await adminClient.products.get('prod_k5nR8xLq', {
    expand: ['variants'],
  })
  ```

  ```bash cURL theme={"theme":"night-owl"}
  curl 'https://api.mystore.com/api/v3/store/products/spree-tee?expand=option_types,option_values' \
    -H 'X-Spree-API-Key: pk_xxx'
  ```
</CodeGroup>

<Info>
  Option type `name` and `label` fields are translatable.
</Info>

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:

<CodeGroup>
  ```typescript Admin SDK theme={"theme":"night-owl"}
  const optionType = await client.optionTypes.create({
    name: 'size',
    label: 'Size',
    option_values: [
      { name: 'small', label: 'Small', position: 1 },
      { name: 'medium', label: 'Medium', position: 2 },
      { name: 'large', label: 'Large', position: 3 },
    ],
  })
  ```

  ```bash CLI theme={"theme":"night-owl"}
  spree api post /option_types -d '{
    "name": "size",
    "label": "Size",
    "option_values": [
      { "name": "small", "label": "Small" },
      { "name": "medium", "label": "Medium" }
    ]
  }'
  ```
</CodeGroup>

## 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:

<CodeGroup>
  ```typescript Store SDK theme={"theme":"night-owl"}
  // 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
  })
  ```

  ```typescript Admin SDK theme={"theme":"night-owl"}
  const { data: products } = await adminClient.products.list({ limit: 12 })
  ```

  ```bash cURL theme={"theme":"night-owl"}
  # thumbnail_url is always in the response — no ?expand needed
  curl 'https://api.mystore.com/api/v3/store/products?limit=12' \
    -H 'X-Spree-API-Key: pk_xxx'
  ```
</CodeGroup>

<Warning>
  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.
</Warning>

### All Images

On the product detail page, expand `media` and `variants` to get the full set of images. Images are ordered by `position`:

<CodeGroup>
  ```typescript Store SDK theme={"theme":"night-owl"}
  const product = await client.products.get('spree-tote', {
    expand: ['media', 'variants'],
  })

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

  // Each variant has its own thumbnail and media_count
  product.variants?.forEach(variant => {
    variant.thumbnail_url    // "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)
  })
  ```

  ```typescript Admin SDK theme={"theme":"night-owl"}
  const product = await adminClient.products.get('prod_86Rf07xd4z', {
    expand: ['media'],
  })
  ```

  ```bash cURL theme={"theme":"night-owl"}
  curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=media,variants' \
    -H 'X-Spree-API-Key: pk_xxx'
  ```
</CodeGroup>

| Field           | Available on     | Always returned | Description                                 |
| --------------- | ---------------- | :-------------: | ------------------------------------------- |
| `thumbnail_url` | Product          |       Yes       | URL to the product's first media            |
| `thumbnail_url` | Variant          |       Yes       | URL to the variant's first media            |
| `media_count`   | Variant          |       Yes       | Number of media                             |
| `media`         | Product, Variant |        No       | Full image array (requires `?expand=media`) |

## Prices

Each variant can have multiple prices — one per currency, plus additional prices from [Price Lists](/developer/core-concepts/pricing) 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:

| Field            | Description                                            |
| ---------------- | ------------------------------------------------------ |
| `price`          | Current selling price                                  |
| `original_price` | Compare-at price (for showing strikethrough discounts) |

See the [Pricing](/developer/core-concepts/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.

<CodeGroup>
  ```typescript Store SDK theme={"theme":"night-owl"}
  // 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,
  })
  ```

  ```typescript Admin SDK theme={"theme":"night-owl"}
  const { data: categories } = await adminClient.categories.list()
  ```

  ```bash cURL theme={"theme":"night-owl"}
  # List categories
  curl 'https://api.mystore.com/api/v3/store/categories' \
    -H 'X-Spree-API-Key: pk_xxx'

  # Get a category by permalink
  curl 'https://api.mystore.com/api/v3/store/categories/clothing/shirts' \
    -H 'X-Spree-API-Key: pk_xxx'

  # List products in a category
  curl 'https://api.mystore.com/api/v3/store/categories/clothing/shirts/products?limit=12' \
    -H 'X-Spree-API-Key: pk_xxx'
  ```
</CodeGroup>

<Info>
  Category `name` and `description` fields are translatable.
</Info>

## Publications and Sales Channels <Since version="5.5" />

A product is visible on a [Channel](/developer/core-concepts/channels) 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.

| Publication state               | What customers see                         |
| ------------------------------- | ------------------------------------------ |
| No publication exists           | Product is not on this channel — invisible |
| Publication has no dates set    | Live now and indefinitely                  |
| `published_at` is in the future | Scheduled — not yet visible                |
| `unpublished_at` is in the past | Hidden — was visible, now sunset           |
| Within the window               | Live                                       |

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.

<CodeGroup>
  ```typescript Admin SDK theme={"theme":"night-owl"}
  const product = await adminClient.products.get('prod_abc', {
    expand: ['product_publications', 'channels'],
  })
  ```

  ```bash cURL theme={"theme":"night-owl"}
  curl 'https://api.mystore.com/api/v3/admin/products/prod_abc?expand=product_publications,channels' \
    -H 'X-Spree-API-Key: sk_xxx'
  ```
</CodeGroup>

```json Response theme={"theme":"night-owl"}
{
  "data": {
    "id": "prod_abc",
    "status": "active",
    "channels": [
      { "id": "ch_online", "code": "online", "name": "Online Store" }
    ],
    "product_publications": [
      {
        "id": "pp_xyz",
        "channel_id": "ch_online",
        "published_at": "2026-07-01T00:00:00Z",
        "unpublished_at": null
      }
    ]
  }
}
```

### 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.

  <CodeGroup>
    ```typescript Admin SDK theme={"theme":"night-owl"}
    await adminClient.products.update('prod_abc', {
      product_publications: [
        { channel_id: 'ch_online' },
        { channel_id: 'ch_pos', published_at: '2026-07-01T00:00:00Z' },
      ],
    })
    ```

    ```bash cURL theme={"theme":"night-owl"}
    curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_abc' \
      -H 'X-Spree-API-Key: sk_xxx' \
      -H 'Content-Type: application/json' \
      -d '{
        "product_publications": [
          { "channel_id": "ch_online" },
          { "channel_id": "ch_pos", "published_at": "2026-07-01T00:00:00Z" }
        ]
      }'
    ```
  </CodeGroup>

* **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.

  <CodeGroup>
    ```typescript Admin SDK theme={"theme":"night-owl"}
    await adminClient.channels.addProducts('ch_online', {
      product_ids: ['prod_abc', 'prod_def'],
      published_at: '2026-07-01T00:00:00Z',
    })

    await adminClient.channels.removeProducts('ch_online', {
      product_ids: ['prod_abc'],
    })
    ```

    ```bash cURL theme={"theme":"night-owl"}
    curl -X POST 'https://api.mystore.com/api/v3/admin/channels/ch_online/add_products' \
      -H 'X-Spree-API-Key: sk_xxx' \
      -H 'Content-Type: application/json' \
      -d '{
        "product_ids": ["prod_abc", "prod_def"],
        "published_at": "2026-07-01T00:00:00Z"
      }'

    curl -X POST 'https://api.mystore.com/api/v3/admin/channels/ch_online/remove_products' \
      -H 'X-Spree-API-Key: sk_xxx' \
      -H 'Content-Type: application/json' \
      -d '{ "product_ids": ["prod_abc"] }'
    ```
  </CodeGroup>

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:

<CodeGroup>
  ```typescript Store SDK theme={"theme":"night-owl"}
  // Client-level default
  const client = createClient({ baseUrl, publishableKey, channel: 'pos' })

  // Per-request override
  const posProducts = await client.products.list({}, { channel: 'pos' })
  ```

  ```typescript Admin SDK theme={"theme":"night-owl"}
  // filter by the channel's code via Ransack (q[channels_code_eq])
  const { data: products } = await adminClient.products.list({ channels_code_eq: 'pos' })
  ```

  ```bash cURL theme={"theme":"night-owl"}
  curl 'https://api.mystore.com/api/v3/store/products' \
    -H 'X-Spree-API-Key: pk_xxx' \
    -H 'X-Spree-Channel: pos'
  ```
</CodeGroup>

For Admin API filtering across channels (back-office reports, admin UI lists), use Ransack instead: `q[channels_id_in][]=ch_xxx`. See [Sales Channels](/developer/core-concepts/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](/developer/core-concepts/channels) for the full channel lifecycle, including default-channel resolution and the `X-Spree-Channel` header.

## Related Documentation

* [Sales Channels](/developer/core-concepts/channels) — Channels, publications, and order attribution
* [Pricing](/developer/core-concepts/pricing) — Price Lists, Price Rules, and market-specific pricing
* [Inventory](/developer/core-concepts/inventory) — Stock management and backorders
* [Media](/developer/core-concepts/media) — Image management
* [Translations](/developer/core-concepts/translations) — Translating product content
* [Search & Filtering](/developer/core-concepts/search-filtering) — Full-text search and Ransack filtering
* [Store SDK Products](/developer/sdk/store/products) — Listing, fetching, filtering, and categories via `client.products`
* [Querying](/api-reference/store-api/querying) — API filtering, sorting, and pagination
