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

Caching

Every endpoint on this page is anonymous and identical for all callers, so responses are cached in Redis for 60 seconds.

  • GET /store/products/:slug is cached per slug and dropped immediately on any write to that product (basics, media, options, variants, tabs, delete) and on any stock adjustment or inventory import that touches it. A slug rename drops both the old and the new key.
  • The store/catalog/* taxonomy reads sit behind a generation counter that any category / brand / tag / ingredient write bumps, so an operator edit invalidates every cached list at once.

Both are best-effort: if Redis is slow or down the request falls through to Postgres. Worst case a read is 60 s stale; nothing here is session-scoped or personalised.

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