Categories, tags, types

Taxonomy reads for navigation trees and filter UIs. Categories are hierarchical (parent_category_id) with optional ancestor/descendant tree embedding; tags and types are flat value lists whose ids feed the tag_id / type_id filters on product listings and search.

SDK module: @barter/storefront/api/categories.

Visibility: only ACTIVE, non-internal categories exist on the store surface — enforced by the anon RLS policy, so an inactive/internal category 404s even when retrieved by id.


GET /api/store/product-categories

  • Purpose — category list / one tree level (pass parent_category_id to walk levels, or the tree flags to embed whole branches).
  • Auth — anon: x-client-id required.
  • Request
// query (all optional)
{
  "q": "appa",                       // case-insensitive substring on name
  "handle": "apparel",               // exact
  "parent_category_id": "pcat_…",    // children of this category
  "include_ancestors_tree": true,    // embed parent_category chains
  "include_descendants_tree": true,  // embed category_children recursively
  "limit": 50,                       // 1–200, default 50
  "offset": 0
}
  • Response{ product_categories, count, offset, limit }, ordered by rank:
{
  "product_categories": [
    {
      "id": "pcat_01tst00000000000000000001",
      "name": "Apparel",
      "handle": "apparel",
      "description": null,
      "parent_category_id": null,
      "rank": 0,
      "is_active": true,        // always true on the store surface
      "is_internal": false,     // always false on the store surface
      "mpath": null,
      "seo_title": null,        // null = fall back to name
      "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",
      // only when include_descendants_tree=true:
      "category_children": [ /* same shape, recursive */ ],
      // only when include_ancestors_tree=true:
      "parent_category": null
    }
  ],
  "count": 1, "offset": 0, "limit": 50
}
  • Working curl — the seeded catalog carries the apparel category:
CATEGORIES=$(curl -sf "$BASE/api/store/product-categories?handle=apparel" \
  -H "x-client-id: $CLIENT_ID")
echo "$CATEGORIES" | grep -q '"product_categories"'
echo "$CATEGORIES" | grep -q '"handle":"apparel"'
CAT_ID=$(echo "$CATEGORIES" | grep -o '"id":"pcat_[^"]*"' | head -1 | cut -d'"' -f4)
test -n "$CAT_ID"
  • Errors — 400 missing_client_id, 400 validation_failed.
  • SDKlistCategories(client, query?).
  • Components — category navigation tree, breadcrumbs.
  • Settings — Admin → Categories: is_active / is_internal flags, rank ordering, SEO overrides.

GET /api/store/product-categories/:id

  • Purpose — retrieve one category, optionally with its tree (category landing page + breadcrumbs in one call).
  • Auth — anon: x-client-id required.
  • Request — query { include_ancestors_tree?, include_descendants_tree? }.
  • Response{ "product_category": { ... } } (tree fields only when requested; trees contain visible categories only).
  • Working curl
CATEGORY=$(curl -sf \
  "$BASE/api/store/product-categories/$CAT_ID?include_descendants_tree=true" \
  -H "x-client-id: $CLIENT_ID")
echo "$CATEGORY" | grep -q '"product_category"'
echo "$CATEGORY" | grep -q '"category_children"'
  • Errors — 404 not_found (unknown, deleted, or inactive/internal — RLS hides them even by id).
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/product-categories/pcat_doesnotexist$RUN" \
  -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 404
  • SDKretrieveCategory(client, categoryId, query?).
  • Components — category page header, breadcrumbs.
  • Settings — as the list.

GET /api/store/product-tags

  • Purpose — tag list (filter chips). Use the returned ids as tag_id filters on /products and /products/search.
  • Auth — anon: x-client-id required.
  • Request — query { q?, value?, limit?, offset? } (q substring, value exact; limit 1–200, default 50).
  • Response — ordered by value:
{
  "product_tags": [
    { "id": "ptag_…", "value": "summer", "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 dev tenant seeds no tags, so this asserts the envelope (the shape contract), not contents:
TAGS=$(curl -sf "$BASE/api/store/product-tags" -H "x-client-id: $CLIENT_ID")
echo "$TAGS" | grep -q '"product_tags"'
echo "$TAGS" | grep -q '"count"'
  • Errors — 400 missing_client_id, 400 validation_failed; retrieve (GET /api/store/product-tags/:id{ "product_tag": …}) 404s on unknown ids:
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/product-tags/ptag_doesnotexist$RUN" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 404
  • SDKlistProductTags(client, query?) / retrieveProductTag(client, tagId).
  • Components — filter sidebar chips.
  • Settings — tags are authored on products in admin.

GET /api/store/product-types

  • Purpose — type list; ids feed type_id filters on listings/search.
  • Auth — anon: x-client-id required.
  • Request — query { q?, value?, limit?, offset? } (same semantics as tags).
  • Response{ product_types, count, offset, limit }, rows { id, value, metadata, created_at, updated_at }, ordered by value.
  • Working curl
TYPES=$(curl -sf "$BASE/api/store/product-types" -H "x-client-id: $CLIENT_ID")
echo "$TYPES" | grep -q '"product_types"'
echo "$TYPES" | grep -q '"count"'
  • Errors — 400 missing_client_id, 400 validation_failed; retrieve (GET /api/store/product-types/:id{ "product_type": …}) 404s on unknown ids:
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/product-types/ptyp_doesnotexist$RUN" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 404
  • SDKlistProductTypes(client, query?) / retrieveProductType(client, typeId).
  • Components — filter sidebar.
  • Settings — types are authored on products in admin.