Collections & membership listings
Curated product groupings (manual or smart). The membership listing —
/collections/:id/products — is the collection page's data source: it reads
the membership JOIN (multi-collection products appear in every collection
they belong to), honors the collection's default_sort, and accepts a
per-request order override. Money is EUR decimal major units.
SDK module: @barter/storefront/api/collections.
Channel scope (Shopify publish-to-channel semantics): a collection with
sales-channel links is visible ONLY on those channels; a collection with no
links is visible everywhere. Pass your channel as sales_channel_id — the
list excludes scoped-away collections, and the membership listing 404s them.
GET /api/store/collections
- Purpose — list collections (navigation, collection index pages).
- Auth — anon:
x-client-idrequired. - Request
// query (all optional)
{
"q": "essen", // case-insensitive substring on title
"handle": "essentials", // exact — THE handle lookup (no /:handle route)
"sales_channel_id": "sc_…", // channel scope (see above)
"limit": 50, // 1–200, default 50
"offset": 0
}- Response —
{ collections, count, offset, limit }, ordered by title:
{
"collections": [
{
"id": "pcol_01tst00000000000000000001",
"title": "Essentials",
"handle": "essentials",
"type": "manual", // "manual" | "smart"
"description": null,
"image_url": null,
"default_sort": "manual", // used by /products when no order override
"conditions": [], // smart-collection rules (admin-authored)
"match": "all", // smart matching: "all" | "any"
"seo_title": null, // null = fall back to title
"seo_description": null, // null = fall back to description
"metadata": null,
"created_at": "2026-07-01T00:00:00.000Z",
"updated_at": "2026-07-01T00:00:00.000Z"
}
],
"count": 1, "offset": 0, "limit": 50
}- Working curl — the seeded catalog carries the
essentialscollection:
COLLECTIONS=$(curl -sf "$BASE/api/store/collections?handle=essentials" \
-H "x-client-id: $CLIENT_ID")
echo "$COLLECTIONS" | grep -q '"collections"'
echo "$COLLECTIONS" | grep -q '"handle":"essentials"'
COL_ID=$(echo "$COLLECTIONS" | grep -o '"id":"pcol_[^"]*"' | head -1 | cut -d'"' -f4)
test -n "$COL_ID"- Errors — 400
missing_client_id, 400validation_failed. - SDK —
listCollections(client, query?). - Components — navigation, collection index grid.
- Settings — collection channel links; smart-collection conditions (membership recomputes on rule/product change).
GET /api/store/collections/:id
- Purpose — retrieve one collection (header/SEO block of a collection
page). By-handle lookup goes through the list (
?handle=). - Auth — anon:
x-client-idrequired. - Request — no query. NOTE (code truth): the single read takes no
sales_channel_id— channel scope applies to the list and the membership listing, not here. - Response —
{ "collection": { ...same shape as list rows... } } - Working curl
COLLECTION=$(curl -sf "$BASE/api/store/collections/$COL_ID" -H "x-client-id: $CLIENT_ID")
echo "$COLLECTION" | grep -q '"collection"'
echo "$COLLECTION" | grep -q '"default_sort"'- Errors — 404
not_found.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
"$BASE/api/store/collections/pcol_doesnotexist$RUN" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 404- SDK —
retrieveCollection(client, collectionId). - Components — collection page header.
- Settings — SEO overrides.
GET /api/store/collections/:id/products
- Purpose — the collection page's product grid: membership join,
published products only, ordered by the collection's
default_sortwith an optionalorderoverride. - Auth — anon:
x-client-id; optional Bearer JWT (group pricing). - Request
// query (all optional)
{
"order": "price_asc", // override: manual | title_asc | title_desc |
// price_asc | price_desc | newest | oldest |
// best_selling (90-day aggregate).
// Unknown values are IGNORED (default_sort used).
"sales_channel_id": "sc_…", // a collection scoped to OTHER channels 404s
"currency_code": "eur", // pricing context → calculated_price
"region_id": "reg_…",
"limit": 50, // 1–100, default 50
"offset": 0
}- Response —
{ products, count, offset, limit }— products carry the FULL canonical product shape (see products.md), incl.calculated_pricewhen a pricing context is given.countis the visible membership size. - Working curl — seeded membership (backfilled from
products.collection_id) contains the three fixture products:
MEMBERS=$(curl -sf "$BASE/api/store/collections/$COL_ID/products?currency_code=eur" \
-H "x-client-id: $CLIENT_ID")
echo "$MEMBERS" | grep -q '"products"'
echo "$MEMBERS" | grep -q '"handle":"linen-shirt"'
echo "$MEMBERS" | grep -q '"calculated_price"'# Sort override: price_asc puts the Wool Beanie (EUR 23) before the Linen
# Shirt (EUR 45) — asserted as RELATIVE order so unrelated rows can't break it.
curl -sf "$BASE/api/store/collections/$COL_ID/products?order=price_asc" \
-H "x-client-id: $CLIENT_ID" | node -e "
const c=[];process.stdin.on('data',d=>c.push(d)).on('end',()=>{
const j=JSON.parse(Buffer.concat(c));
const h=j.products.map(p=>p.handle);
const a=h.indexOf('wool-beanie'), b=h.indexOf('linen-shirt');
if(a<0||b<0||a>b){console.error('price_asc order wrong: '+h.join(','));process.exit(1)}
})"- Errors — 404
not_found(unknown collection, or scoped away from the givensales_channel_id), 400validation_failed, 400invalid_region. - SDK —
listCollectionProducts(client, collectionId, query?). - Components — product card grid + sort dropdown (emit the
ordervalues above). - Settings — collection
default_sort+ manual position order (drag-reorder in admin); price lists; channel links.