Supercommerce API Docs
Store API

Dynamic Product Listing Module — Storefront

Public storefront surface for reading dynamic product listing groups by slug and resolving dynamic product listings (saved filter specs) into live product results from the search engine.

HTTP surface for reading dynamic product listing groups by slug and resolving dynamic product listings (saved filter specs) into live product results. Groups and listings are created and managed via the admin surface; the storefront reads them here.

Source: api-modules/dynamic-product-listing/src/controllers/store-dynamic-product-listing-group.controller.ts, api-modules/dynamic-product-listing/src/controllers/store-dynamic-product-listing.controller.ts.

Admin CRUD for groups and listings is in docs/separated/admin/dynamic-product-listing.md. The storefront resolves a listing's saved filters into matching products at request time, with optional per-request overrides.


Conventions

Authentication

EndpointAuth
GET /store/dynamic-product-listing-groupsnone (public)
GET /store/dynamic-product-listing-groups/slug/:slugnone (public)
GET /store/dynamic-product-listings/slug/:slugnone (public)
GET /store/dynamic-product-listings/slug/:slug/productsnone (public)

All storefront endpoints are public — no session required.

Response envelope (standard endpoints)

Standard group/listing reads are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* pagination: total, limit, offset, hasMore */ }
}

The /products endpoint passes the product-search response verbatim — it uses the legacy page-based search envelope and is not wrapped by ResponseInterceptor:

{
  "data": {
    "products": [ /* product objects */ ],
    "brands": [ /* facet */ ],
    "ingredients": [ /* facet */ ],
    "attributes": { /* facet map */ }
  },
  "metadata": {
    "total": 120,
    "items": 20,
    "perPage": 20,
    "currentPage": 1,
    "lastPage": 6
  }
}

This is the legacy page-based pagination shape (perPage / currentPage / lastPage), not the standard offset-based { total, limit, offset, hasMore } used elsewhere. It is intentional — the /products endpoint is a verbatim passthrough to the product-search engine.

Error envelope

statusCodeerrorCode examples
404NOT_FOUND
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Domain types

DynamicProductListingGroupResponse

type DynamicProductListingGroupResponse = {
  id: string;
  title: string;
  slug: string;                                // lowercase alnum + hyphens
  metadata: Record<string, unknown> | null;
  createdAt: string;                           // ISO
  updatedAt: string;
};

type DynamicProductListingGroupWithListingsResponse = DynamicProductListingGroupResponse & {
  listings: DynamicProductListingResponse[];   // nested listings with filters
};

DynamicProductListingResponse

type DynamicProductListingResponse = {
  id: string;
  title: string;
  slug: string;
  groupId: string | null;
  subtitle: string | null;
  description: string | null;
  image: string | null;
  order: number;
  filters: ListingFilters | null;              // saved filter spec
  metadata: Record<string, unknown> | null;
  createdAt: string;
  updatedAt: string;
};

Endpoints

GET /store/dynamic-product-listing-groups — List groups

Returns all groups (paginated). Standard offset pagination.

Query params

NameTypeNotes
limitint?Default 20
offsetint?Default 0

Response 200 — paginated envelope of DynamicProductListingGroupResponse[].


GET /store/dynamic-product-listing-groups/slug/:slug — Get a group with its listings

Returns the group plus its nested listings array (each listing includes its filters field). Used to build a collections page or a carousel of curated filter-driven product shelves.

Path params

NameNotes
slugGroup slug (lowercase alnum + hyphens)

Response 200DynamicProductListingGroupWithListingsResponse.

{
  "data": {
    "id": "01J9aaaa...",
    "title": "Summer Collection",
    "slug": "summer-collection",
    "metadata": null,
    "createdAt": "2026-04-01T08:00:00.000Z",
    "updatedAt": "2026-05-01T08:00:00.000Z",
    "listings": [
      {
        "id": "01J9bbbb...",
        "title": "Budget Supplements",
        "slug": "budget-supplements",
        "groupId": "01J9aaaa...",
        "subtitle": "Great value picks",
        "description": null,
        "image": "listings/budget-supplements.jpg",
        "order": 0,
        "filters": {
          "categories": ["supplements"],
          "maxPrice": 50000,
          "inStock": true,
          "sortBy": "price-asc"
        },
        "metadata": null,
        "createdAt": "2026-04-15T08:00:00.000Z",
        "updatedAt": "2026-04-15T08:00:00.000Z"
      }
    ]
  },
  "message": "Success",
  "statusCode": 200
}

Errors

StatusCodeWhen
404NOT_FOUNDNo group matches the slug

GET /store/dynamic-product-listings/slug/:slug — Get one listing

Returns a single listing's metadata including its saved filters spec, without resolving products. Use this to display the listing's title / image / description and then call /products separately.

Path params

NameNotes
slugListing slug

Response 200DynamicProductListingResponse.

Errors

StatusCodeWhen
404NOT_FOUNDNo listing matches the slug

GET /store/dynamic-product-listings/slug/:slug/products — Resolve listing into products

Fetches the listing's saved filters spec and passes it to the product-search engine, returning matching products and facets. Runtime query params can override the saved spec on a per-field basis — useful for client-side sort/filter controls on a listing page.

page and limit always come from the request (never from the saved spec).

Envelope: the response is passed verbatim from the product-search engine — the legacy page-based metadata: { total, items, perPage, currentPage, lastPage } shape. This is intentional and differs from the standard offset-based pagination used elsewhere.

Path params

NameNotes
slugListing slug

Query params — all optional; any supplied field overrides the corresponding field in the listing's saved filters:

NameTypeNotes
qstring?Full-text search term
vendorsstring?CSV of vendor ids
brandsstring?CSV, e.g. acme,goodlife
categoriesstring?CSV
tagsstring?CSV
ingredientsstring?CSV
skusstring?CSV
attributesstring?JSON string, e.g. {"flavour":["mango"]}
minPriceint?Integer subunits
maxPriceint?Integer subunits
inStock"true" | "false"?String boolean
hasActiveSpecial"true" | "false"?String boolean
withReviews"true" | "false"?Embed each product's latest approved reviews (reviews[], up to 10). Also settable as a saved option on the listing's filters; the runtime param overrides it.
sortBystring?relevance | price-asc | price-desc | new | best-selling | inventory-high | inventory-low | rating-desc | rating-asc
pageint?Default 1; always from request
limitint?Default 20; always from request

Response 200 — verbatim product-search engine envelope:

{
  "data": {
    "products": [
      {
        "id": "01J9cccc...",
        "title": "Whey Protein 1kg",
        "slug": "whey-protein-1kg",
        "price": 189900,
        "rating": { "average": 4.5, "count": 12 }
        // ... standard product fields
      }
    ],
    "brands": [ { "id": "acme", "label": "Acme Nutrition", "count": 12 } ],
    "ingredients": [],
    "attributes": { "flavour": [ { "value": "mango", "count": 4 } ] }
  },
  "metadata": {
    "total": 120,
    "items": 20,
    "perPage": 20,
    "currentPage": 1,
    "lastPage": 6
  }
}

Errors

StatusCodeWhen
404NOT_FOUNDNo listing matches the slug

  • storage — resolves image keys to CDN URLs on the client.
  • search — the underlying product-search engine; the /products envelope is its native format.
  • admin/dynamic-product-listing — admin CRUD for groups and listings. See admin/dynamic-product-listing.md.

On this page