> ## 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](/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`          |
| `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](/developer/core-concepts/shipments#order-routing) 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_…`).

<CodeGroup>
  ```typescript 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' })
  ```

  ```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 (`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](/developer/core-concepts/shipments#order-routing).

## 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 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 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](/developer/core-concepts/stores) — Channels belong to a store
* [Markets](/developer/core-concepts/markets) — Different from channels: markets segment geography/currency, channels segment selling surfaces
* [Products](/developer/core-concepts/products) — Product catalog and publication
* [Order Routing](/developer/core-concepts/shipments#order-routing) — Channels can override the store's routing strategy
