docs/developer/core-concepts/channels.mdx
import { Since } from '/snippets/since.mdx';
<Since version="5.5" />Channels segment a single Store 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.
| 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 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.
Every incoming Store API or storefront request resolves to a channel:
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.The resolved channel is then available to controllers, models, and serializers throughout the request.
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.
// 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' })
// admin filters by the channel's code via Ransack (q[channels_code_eq])
const { data: products } = await adminClient.products.list({ channels_code_eq: 'pos' })
curl 'https://api.mystore.com/api/v3/store/products' \
-H 'X-Spree-API-Key: pk_xxx' \
-H 'X-Spree-Channel: pos'
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).
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.
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.
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….
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 |
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',
})
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"
}'
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:
await adminClient.products.update('prod_xxx', {
product_publications: [
{ channel_id: 'ch_online' },
{ channel_id: 'ch_pos', published_at: '2026-07-01T00:00:00Z' },
],
})
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" }
]
}'
The write contract is full-set: the array represents the complete desired state. Channels absent from the payload are detached.
product_publications: [{ channel_id }] on create, or calls POST /admin/channels/:id/add_products afterwards.bin/rake spree:load_sample_data) — all loaded products are explicitly published on the default channel.adminClient.channels.addProducts and other resource methods are structured