Supercommerce API Docs
Store API

Catalog Module — Storefront

HTTP surface for unauthenticated catalog reads. The storefront uses these endpoints to render the navigation tree, brand / tag / ingredient pages, the product detail page (PDP),…

HTTP surface for unauthenticated catalog reads. The storefront uses these endpoints to render the navigation tree, brand / tag / ingredient pages, the product detail page (PDP), and to resolve slugs to ids for deep-linking. Only active, non-deleted rows are visible — admin and vendor product/variant management lives in sibling docs.

Source: api-modules/catalog/src/controllers/public-catalog.controller.ts (taxonomy reads), api-modules/catalog/src/controllers/store-product.controller.ts (product detail).

The four taxonomies (categories, brands, tags, ingredients) share a single response shape (CatalogItemResponse). Cart, order, and inventory consume product+variant data via this module's services; HSN classification lives on product_variant, not the product, because variants in the same family can carry different HSN codes.


Conventions

Authentication

Endpoint groupAuth
GET /store/catalog/**none (public)
GET /store/products/:slugnone (public)

No session is required. Soft-deleted (deletedAt IS NOT NULL) and inactive (isActive = false) rows are filtered server-side; the storefront never sees them.

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* pagination on list endpoints */ }
}

Paginated lists use the standard metadata: { total, limit, offset, hasMore } shape.

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR (bad query coercion)
404NOT_FOUND
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

QueryDto

List endpoints accept the platform's standard QueryDto:

NameTypeDefaultNotes
searchValuestring?Combined with searchField + searchOperator (contains / starts_with / ends_with)
searchFieldstring?Allowed fields are per-service (typically title, slug)
limitint1001..500
offsetint0≥ 0
sortBy, sortDirectionstring?, asc/desc, descAllowed sort fields are per-service
filters[]JSON?[{ field, value, operator }]; operator is one of eq, ne, lt, lte, gt, gte, in, not_in, contains, starts_with, ends_with

Domain types

CatalogItemResponse

Shared by brand / category / tag / ingredient.

type CatalogItemResponse = {
  id: string;
  title: string;
  description: string | null;
  slug: string;                            // lowercase alnum + hyphens
  image: string | null;
  metadata: Record<string, unknown> | null;
  isActive: boolean;                       // always true for storefront reads
  createdAt: string;                       // ISO
  updatedAt: string;
  deletedAt: string | null;                // always null for storefront reads
};

Categories

Base path: /store/catalog/categories.

GET /store/catalog/categories — List active categories

Paginated. Accepts the standard QueryDto.

Response 200 — paginated CatalogItemResponse[].


GET /store/catalog/categories/tree — Active category tree

Returns the full parent/child hierarchy in a single call, materialized server-side. Inactive and soft-deleted categories (and their subtrees) are pruned. Not paginated.

Response 200 — array of category nodes with nested children: CategoryNode[] per the category service's tree shape.


GET /store/catalog/categories/slug/:slug — Get a category by slug

Path params

NameNotes
slugLowercase alnum + hyphens. The service filters to isActive=true, deletedAt IS NULL

Response 200CatalogItemResponse.

Errors

StatusCodeWhen
404NOT_FOUNDNo active category with that slug

GET /store/catalog/categories/:id — Get a category by id

Response 200CatalogItemResponse.

Errors

StatusCodeWhen
404NOT_FOUNDId does not exist, or row is inactive / soft-deleted

Brands

Base path: /store/catalog/brands. Same shape as categories — minus the tree endpoint.

GET /store/catalog/brands — List active brands

Paginated QueryDto. Response 200 — paginated CatalogItemResponse[].

GET /store/catalog/brands/slug/:slug

Response 200CatalogItemResponse. 404 if the brand is missing / inactive / deleted.

GET /store/catalog/brands/:id

Response 200CatalogItemResponse. 404 if the brand is missing / inactive / deleted.


Tags

Base path: /store/catalog/tags. Same shape as brands.

GET /store/catalog/tags — List active tags

Paginated QueryDto. Response 200 — paginated CatalogItemResponse[].

GET /store/catalog/tags/slug/:slug

Response 200CatalogItemResponse. 404 if the tag is missing / inactive / deleted.

GET /store/catalog/tags/:id

Response 200CatalogItemResponse. 404 if the tag is missing / inactive / deleted.


Ingredients

Base path: /store/catalog/ingredients. Same shape as brands.

GET /store/catalog/ingredients — List active ingredients

Paginated QueryDto. Response 200 — paginated CatalogItemResponse[].

GET /store/catalog/ingredients/slug/:slug

Response 200CatalogItemResponse. 404 if the ingredient is missing / inactive / deleted.

GET /store/catalog/ingredients/:id

Response 200CatalogItemResponse. 404 if the ingredient is missing / inactive / deleted.


Products (PDP)

Base path: /store/products. The public product-detail surface that backs the storefront PDP. Listing and faceted discovery live in search.md; this endpoint is the single-product hydrate once a slug is known.

GET /store/products/:slug — Product detail by slug

Returns a store-visible product with its options, variants, content tabs, taxonomy id arrays, and a slim vendor summary. Visibility gating — only a product that is status=active, visibility=public, non-deleted, and published (publishedAt IS NOT NULL AND publishedAt <= now()) is returned; anything else is a 404 (the same neutral 404 as an unknown slug, so unpublished/hidden products can't be probed).

Path params

NameNotes
slugProduct slug. The public PDP URL is /product/{slug} (singular) on the storefront; this API path is store/products/:slug.

Response 200StoreProductDetail:

type StoreVendorSummary = {
  id: string;
  name: string;
  slug: string;
  logo: string | null;
};

type StoreVariantStock = {
  availableQuantity: number | null;  // null = withheld (see below)
  isOrderable: boolean;              // can this be added to a cart right now
  status: "in_stock" | "low_stock" | "out_of_stock" | "backorder" | "untracked";
};

type StoreProductVariantDetail = {
  // …price/specialPrice (subunits), sku/ean/upc/barcode, hsnCode,
  // min/max per cart, option-value refs…
  stock: StoreVariantStock;
};

type StoreProductDetail = {
  product: ProductRecord;                 // core product columns (title, subtitle,
                                          // description, brandId, thumbnail, images,
                                          // SEO meta, hsCode, etc.)
  options: ProductOptionDetail[];         // option groups + values (e.g. Size, Color)
  variants: StoreProductVariantDetail[];  // purchasable variants + their stock view
  tabs: TabRecord[];                      // rich-content tabs (description, how-to, etc.)
  categoryIds: string[];
  tagIds: string[];
  ingredientIds: string[];
  vendor: StoreVendorSummary;
  soldLastMonth: number | null;
};

The product / options / tabs sub-shapes are identical to the ProductDetail base returned by the admin/vendor product-detail endpoints — see ../admin/catalog.md for the full field-level breakdown. The storefront variant differs in the gating above, the slim StoreVendorSummary (no admin-only vendor fields), and stock in place of the admin inventory snapshot. Prices on variants are integer subunits.

Variant stock

stock is a deliberately narrow projection of the inventory snapshot. The operational counts — on-hand, reserved, safety stock — and the backorder allowance are never exposed to a storefront caller; only these three fields are.

Render off status and isOrderable, never off the number. availableQuantity is null in two different situations that both mean "no count to show":

  • the variant isn't tracked (status: "untracked"), so no finite number exists; or
  • the operator has store/product_cart.show_stock_quantity off, which withholds the count server-side — the field is null in the payload rather than merely hidden by the client.

A variant can be orderable with a zero or null count: backorder and untracked are both isOrderable: true. Conversely low_stock and in_stock are both orderable — low_stock is a display hint, not a restriction.

low_stock is decided by the vendor's per-variant lowStockThreshold when they set one; otherwise by the store-wide store/product_cart.low_stock_threshold (admin-only, 0 disables the fallback).

When the variant is not orderable, the shopper can ask to be told about the next restock — see Back in Stock.

soldLastMonth is the per-product "units sold last month" badge count, already display-transformed by any admin-configured override rules — see ../admin/product-sold-metrics.md. It is null whenever the operator has the feature turned off (store/sold_last_month.enabled setting) or no monthly computation has run yet. The raw/actual count and the override rules themselves are never exposed here — only the final number.

Errors

StatusCodeWhen
404NOT_FOUNDNo product with that slug, or it is inactive / non-public / unpublished / soft-deleted, or its vendor cannot be resolved

  • search — actual product listing and faceting for the storefront. This module only exposes taxonomy reads + single-product detail; product search/filter lives in search.md.
  • bannerGET /store/banners/:entityType/slug/:slug accepts the same slug values exposed by this surface. See banner.md.
  • cart — variants added to the cart come from this module's product+variant tables.

On this page