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_idto walk levels, or the tree flags to embed whole branches). - Auth — anon:
x-client-idrequired. - 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 byrank:
{
"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
apparelcategory:
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, 400validation_failed. - SDK —
listCategories(client, query?). - Components — category navigation tree, breadcrumbs.
- Settings — Admin → Categories:
is_active/is_internalflags, 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-idrequired. - 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- SDK —
retrieveCategory(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_idfilters on/productsand/products/search. - Auth — anon:
x-client-idrequired. - Request — query
{ q?, value?, limit?, offset? }(qsubstring,valueexact;limit1–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, 400validation_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- SDK —
listProductTags(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_idfilters on listings/search. - Auth — anon:
x-client-idrequired. - 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, 400validation_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- SDK —
listProductTypes(client, query?)/retrieveProductType(client, typeId). - Components — filter sidebar.
- Settings — types are authored on products in admin.