Back to Spree

Channels

docs/developer/core-concepts/channels.mdx

5.6.011.4 KB
Original Source

import { Since } from '/snippets/since.mdx';

<Since version="5.5" />

Overview

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.

Channel Attributes

AttributeDescriptionExample
nameHuman-readable name, displayed in the admin and reportsPoint of Sale
codeURL-safe slug, stable identifier sent via the X-Spree-Channel headerpos
activeWhen false, the channel stops accepting orderstrue
defaultExactly 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 productstrue
storefront_accessControls what an anonymous visitor may see: public, prices_hidden, or login_required. Unset inherits the store's setting. See Storefront Access Gatinglogin_required
guest_checkoutWhether an order may be placed without an account on this channel. Unset inherits the store's settingfalse
preferred_order_routing_strategyOptional per-channel override of the store's Order Routing strategySpree::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.

<CodeGroup>
typescript
// 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 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 '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 stateWhat customers see
No publication existsProduct is not on this channel — invisible
Publication has no dates setLive now and indefinitely
published_at is in the futureScheduled — not yet visible
unpublished_at is in the pastHidden — was visible, now sunset
Within the windowLive

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.

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:

ModeGuest sees catalogGuest sees pricesUse case
publicYesYesThe default. An open storefront — anyone can browse; guests can also check out when guest_checkout is enabled.
prices_hiddenYesNo — prices come back nullA catalog you want discoverable, with pricing revealed only after sign-in (e.g. a trade catalog for lead generation).
login_requiredNo — reads rejected with 401NoA 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 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
// 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 -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 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:

EndpointUse case
POST /api/v3/admin/channels/:id/add_productsPublish one or more products on a specific channel
POST /api/v3/admin/channels/:id/remove_productsUnpublish products from a specific channel
POST /api/v3/admin/products/bulk_add_to_channelsPublish many products across many channels in a single request
<CodeGroup>
typescript
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 -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
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 -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.
  • Stores — Channels belong to a store
  • Markets — Different from channels: markets segment geography/currency, channels segment selling surfaces
  • Products — Product catalog and publication
  • Order Routing — Channels can override the store's routing strategy
  • Store SDK: Products — Channel-scoped product listing and filtering
  • Admin SDK: Resources — How adminClient.channels.addProducts and other resource methods are structured
  • Wholesale Portal — A gated channel driving the Next.js storefront's B2B surface