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

# Channels

> Per-store distribution surfaces — online storefront, POS, marketplace, wholesale — each with its own product catalog and order attribution.

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>;
};

<Since version="5.5" />

## Overview

Channels segment a single [Store](/docs/developer/core-concepts/stores) into distinct selling surfaces. A channel represents *where* an order originates from — the online storefront, an in-person point-of-sale till, a marketplace integration (Amazon, eBay), a B2B wholesale portal, a mobile app — and *which* subset of the store's products is available there.

Every store ships with one default channel named *Online Store*. You can add more from **Settings → Sales channels** in the admin dashboard.

## Channel Attributes

| Attribute                          | Description                                                                                                                                                                               | Example                                |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `name`                             | Human-readable name, displayed in the admin and reports                                                                                                                                   | `Point of Sale`                        |
| `code`                             | URL-safe slug, stable identifier sent via the `X-Spree-Channel` header                                                                                                                    | `pos`                                  |
| `active`                           | When `false`, the channel stops accepting orders                                                                                                                                          | `true`                                 |
| `default`                          | Exactly one channel per store is the default. Used as a fallback when no channel header is present and as the auto-publish target for new products                                        | `true`                                 |
| `storefront_access`                | Controls what an anonymous visitor may see: `public`, `prices_hidden`, or `login_required`. Unset inherits the store's setting. See [Storefront Access Gating](#storefront-access-gating) | `login_required`                       |
| `guest_checkout`                   | Whether an order may be placed without an account on this channel. Unset inherits the store's setting                                                                                     | `false`                                |
| `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](/docs/developer/core-concepts/shipments#order-routing) strategy                                                                   | `Spree::OrderRouting::Strategy::Rules` |

`code` is normalized to a URL-safe slug on save — `POS` becomes `pos`, `Point of Sale!` becomes `point-of-sale`. Leaving `code` blank derives it from `name`.

## How Channels Work

### Resolution at request time

Every incoming Store API or storefront request resolves to a channel:

1. If the `X-Spree-Channel` header is present, the value is matched against `channels.code` — or `channels.id` when the value looks like a prefixed ID (`ch_…`) — scoped to the current store.
2. Otherwise, the store's default channel is used.

The resolved channel is then available to controllers, models, and serializers throughout the request.

### Selecting a channel from the Store SDK

The Store SDK sends `X-Spree-Channel` on every request when configured. The value can be either the channel `code` (merchant-meaningful, recommended) or the prefixed ID (`ch_…`). `setChannel` is a sticky setter that [mirrors `setLocale` / `setCurrency` / `setCountry`](/docs/developer/sdk/configuration).

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

  // Sticky setter (mirrors setLocale / setCurrency / setCountry)
  client.setChannel('wholesale')

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

  ```typescript Admin SDK theme={"theme":"night-owl"}
  // admin filters 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>

The Admin API does not consume `X-Spree-Channel` — admin endpoints return data across all channels for the current store. [Filter by channel on the admin side via Ransack](/docs/api-reference/admin-api/querying) (`q[channel_id_eq]=ch_xxx` for orders, `q[channels_id_in][]=ch_xxx` for products).

### Product visibility

A product is visible on a channel only when it has a publication record joining the two. Each publication carries an optional window:

| 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. The dashboard's Publishing card renders this as a "Not available" badge on every channel row when status isn't `active`.

### Order attribution

Every order is attributed to one channel. The channel is set from the `X-Spree-Channel` header on cart creation, from the merchant's selection on the "New order" form, or defaults to the store's primary channel.

This attribution drives reporting (best-selling by channel, revenue per channel) and per-channel order routing — see [Order Routing](/docs/developer/core-concepts/shipments#order-routing).

### Storefront Access Gating

<Since version="5.6" />

A channel's `storefront_access` decides what an **anonymous** visitor — a request with no authenticated customer — may see. Logged-in customers are never gated. The posture is one of three values:

| Mode             | Guest sees catalog             | Guest sees prices            | Use case                                                                                                              |
| ---------------- | ------------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `public`         | Yes                            | Yes                          | The default. An open storefront — anyone can browse; guests can also check out when `guest_checkout` is enabled.      |
| `prices_hidden`  | Yes                            | No — prices come back `null` | A catalog you want discoverable, with pricing revealed only after sign-in (e.g. a trade catalog for lead generation). |
| `login_required` | No — reads rejected with `401` | No                           | A fully gated surface — a guest can't read the catalog at all (e.g. a members-only or B2B wholesale portal).          |

The gate is enforced by the **Store API**, not the storefront, so a storefront app can't loosen it:

* **`login_required`** — every gated read returns `401` for an unauthenticated request. Endpoints that must stay reachable before sign-in (authentication, password reset, reference data like countries and currencies) are exempt.
* **`prices_hidden`** — reads succeed, but every money field is serialized as `null` for a guest. The storefront renders these as a sign-in prompt rather than a price.

A companion control, **`guest_checkout`**, decides whether an order can be placed without an account on the channel. It's independent of `storefront_access` — a `public` channel can still require accounts, and the two are resolved separately.

#### Store fallback

Both controls fall back to the owning [Store](/docs/developer/core-concepts/stores) when the channel's own value is unset — the same inheritance pattern as the channel's order-routing strategy. `storefront_access` resolves to the channel value, then the store value, and finally `public` when neither is set. This lets you set a store-wide default (e.g. "all channels require login") and override per channel where needed.

<CodeGroup>
  ```typescript Admin SDK theme={"theme":"night-owl"}
  // Gate a channel behind sign-in, and require accounts at checkout
  await adminClient.channels.update('ch_wholesale', {
    storefront_access: 'login_required',
    guest_checkout: false,
  })

  // Clear the channel value to inherit the store's default
  await adminClient.channels.update('ch_wholesale', {
    storefront_access: null,
  })
  ```

  ```bash cURL theme={"theme":"night-owl"}
  curl -X PATCH 'https://api.mystore.com/api/v3/admin/channels/ch_wholesale' \
    -H 'X-Spree-API-Key: sk_xxx' \
    -H 'Content-Type: application/json' \
    -d '{ "storefront_access": "login_required", "guest_checkout": false }'
  ```
</CodeGroup>

Switching a channel between modes takes effect immediately — the posture is resolved per request, so no cache warm-up or redeploy is needed. The Next.js storefront's [wholesale portal](/docs/developer/storefront/nextjs/wholesale) is a worked example of a `login_required` / `prices_hidden` channel driving the UI.

## Publishing Products on Channels

### Dashboard

The product edit page has a **Publishing** card with one row per channel the product is on. Click *Manage* to attach or detach channels via checkboxes. Each row expands into a per-channel schedule editor.

Bulk operations from the product list: *Add to sales channels…* and *Remove from sales channels…*.

### Admin API

Three endpoints cover the publishing surface:

| Endpoint                                           | Use case                                                       |
| -------------------------------------------------- | -------------------------------------------------------------- |
| `POST /api/v3/admin/channels/:id/add_products`     | Publish one or more products on a specific channel             |
| `POST /api/v3/admin/channels/:id/remove_products`  | Unpublish products from a specific channel                     |
| `POST /api/v3/admin/products/bulk_add_to_channels` | Publish many products across many channels in a single request |

<CodeGroup>
  ```typescript Admin SDK theme={"theme":"night-owl"}
  await adminClient.channels.addProducts('ch_xxx', {
    product_ids: ['prod_aaa', 'prod_bbb'],
    // Optional window — when omitted, existing schedules are preserved
    published_at: '2026-07-01T00:00:00Z',
    unpublished_at: '2026-12-31T23:59:59Z',
  })
  ```

  ```bash cURL theme={"theme":"night-owl"}
  curl -X POST 'https://api.mystore.com/api/v3/admin/channels/ch_xxx/add_products' \
    -H 'X-Spree-API-Key: sk_xxx' \
    -H 'Content-Type: application/json' \
    -d '{
      "product_ids": ["prod_aaa", "prod_bbb"],
      "published_at": "2026-07-01T00:00:00Z",
      "unpublished_at": "2026-12-31T23:59:59Z"
    }'
  ```
</CodeGroup>

`channels.addProducts` is idempotent: re-publishing an already-published product is a no-op for its window unless `published_at` / `unpublished_at` are explicitly passed. Cross-store onboarding is allowed when the caller's key has update permission on the product.

For per-product updates, use `PATCH /api/v3/admin/products/:id` with a `product_publications` array:

<CodeGroup>
  ```typescript Admin SDK theme={"theme":"night-owl"}
  await adminClient.products.update('prod_xxx', {
    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_xxx' \
    -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>

The write contract is **full-set**: the array represents the complete desired state. Channels absent from the payload are detached.

## Auto-Publish Behavior

* **Dashboard** — new products are auto-published on the store's default channel. The merchant can untick channels via the Publishing card post-create.
* **Admin API** — new products are **not** auto-published. The caller supplies `product_publications: [{ channel_id }]` on create, or calls `POST /admin/channels/:id/add_products` afterwards.
* **Sample data** (`bin/rake spree:load_sample_data`) — all loaded products are explicitly published on the default channel.

## Related Documentation

* [Stores](/docs/developer/core-concepts/stores) — Channels belong to a store
* [Markets](/docs/developer/core-concepts/markets) — Different from channels: markets segment geography/currency, channels segment selling surfaces
* [Products](/docs/developer/core-concepts/products) — Product catalog and publication
* [Order Routing](/docs/developer/core-concepts/shipments#order-routing) — Channels can override the store's routing strategy
* [Store SDK: Products](/docs/developer/sdk/store/products) — Channel-scoped product listing and filtering
* [Admin SDK: Resources](/docs/developer/sdk/admin/resources) — How `adminClient.channels.addProducts` and other resource methods are structured
* [Wholesale Portal](/docs/developer/storefront/nextjs/wholesale) — A gated channel driving the Next.js storefront's B2B surface
