Supercommerce API Docs
Admin API

Dynamic Product Listing Module — Admin

HTTP surface for managing dynamic product listing groups (named containers) and dynamic product listings (saved filter specs). Groups and listings are addressable by slug; the storefront…

HTTP surface for managing dynamic product listing groups (named containers) and the dynamic product listings (saved filter specs) inside each group. Groups and listings are addressable by slug; the storefront resolves the saved filters into live product results at read time. This admin surface is the only writer.

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

A group is a named, slug-addressable collection of listings. A listing is a reusable filter specification (brands, categories, price range, attributes, etc.) that the storefront resolves into matching products on demand, with optional runtime query overrides.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching permission.

Endpoint groupPermission
GET /admin/dynamic-product-listing-groups, GET /admin/dynamic-product-listing-groups/:iddynamicProductListingGroup: read
POST /admin/dynamic-product-listing-groups, POST /admin/dynamic-product-listing-groups/:id/duplicatedynamicProductListingGroup: create
PUT /admin/dynamic-product-listing-groups/:iddynamicProductListingGroup: update
DELETE /admin/dynamic-product-listing-groups/:iddynamicProductListingGroup: delete
GET /admin/dynamic-product-listings, GET /admin/dynamic-product-listings/:id, GET /admin/dynamic-product-listings/available-filters, POST /admin/dynamic-product-listings/previewdynamicProductListing: read
POST /admin/dynamic-product-listings, POST /admin/dynamic-product-listings/:id/duplicatedynamicProductListing: create
PUT /admin/dynamic-product-listings/:id, PATCH /admin/dynamic-product-listings/reorderdynamicProductListing: update
DELETE /admin/dynamic-product-listings/:iddynamicProductListing: delete

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Paginated lists

List endpoints return the standard offset-based pagination envelope:

{
  "data": [ /* items */ ],
  "message": "Success",
  "statusCode": 200,
  "metadata": { "total": 45, "limit": 20, "offset": 0, "hasMore": true }
}

Query params shared by all list endpoints: searchValue, searchField, limit, offset, sortBy, sortDirection.

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409UNIQUE_VIOLATION (slug collision)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle

Deleting a group does not delete its listings — it detaches them (FK set to null). Deleting a listing is a hard delete.


Domain types

DynamicProductListingGroupResponse

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

type DynamicProductListingGroupWithListingsResponse = DynamicProductListingGroupResponse & {
  listings: DynamicProductListingResponse[];     // nested listing objects
};

DynamicProductListingResponse

type DynamicProductListingResponse = {
  id: string;
  title: string;
  slug: string;
  groupId: string | null;
  subtitle: string | null;
  description: string | null;
  image: string | null;
  order: number;                                 // ASC sort within group, >= 0
  filters: ListingFilters | null;
  metadata: Record<string, unknown> | null;
  createdAt: string;                             // ISO
  updatedAt: string;
};

ListingFilters

All fields optional. Saved as a JSON spec on the listing; the storefront resolves it into live search results.

type ListingFilters = {
  q?: string;                               // free-text search term
  brands?: string[];
  categories?: string[];
  tags?: string[];
  ingredients?: string[];
  skus?: string[];
  attributes?: Record<string, string[]>;    // codeallowed values
  minPrice?: number;                        // integer subunits
  maxPrice?: number;                        // integer subunits
  inStock?: boolean;
  hasActiveSpecial?: boolean;
  withReviews?: boolean;                     // embed each product's latest approved reviews in the resolved response
  sortBy?: "relevance" | "price-asc" | "price-desc" | "new" | "best-selling" | "inventory-high" | "inventory-low" | "rating-desc" | "rating-asc";
};

Dynamic product listing groups

Base path: /admin/dynamic-product-listing-groups.

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

Required permission: dynamicProductListingGroup: read. Standard QueryDto (searchValue, searchField, limit, offset, sortBy, sortDirection).

Response 200 — paginated envelope of DynamicProductListingGroupResponse[].


GET /admin/dynamic-product-listing-groups/:id — Get a group with its listings

Required permission: dynamicProductListingGroup: read. Returns the group with nested listings[].

Response 200DynamicProductListingGroupWithListingsResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/dynamic-product-listing-groups — Create a group

Required permission: dynamicProductListingGroup: create.

Body

{
  "title": "Summer Collection",
  "slug": "summer-collection",
  "metadata": { "platform": "WEB" }
}
FieldTypeConstraints
titlestring1..255
slugstring1..255, lowercase alnum + hyphens
metadataRecord<string, unknown> | null?Free-form

Response 201DynamicProductListingGroupResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
409UNIQUE_VIOLATIONslug already in use

PUT /admin/dynamic-product-listing-groups/:id — Update a group

Required permission: dynamicProductListingGroup: update. Partial body (same fields as create).

Response 200DynamicProductListingGroupResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
409UNIQUE_VIOLATIONslug taken by another group

DELETE /admin/dynamic-product-listing-groups/:id — Delete a group

Required permission: dynamicProductListingGroup: delete. Detaches child listings (sets their groupId to null); does not delete them.

Response 204 No Content.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/dynamic-product-listing-groups/:id/duplicate — Duplicate a group

Required permission: dynamicProductListingGroup: create. Clones the source group with a fresh title + slug. Every child listing is cloned; each clone receives a deduplicated <slug>-copy[-N] slug.

Body

{ "title": "Summer Collection (v2)", "slug": "summer-collection-v2" }
FieldTypeConstraints
titlestring1..255
slugstring1..255, lowercase alnum + hyphens

Response 201DynamicProductListingGroupWithListingsResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDSource group not found
409UNIQUE_VIOLATIONNew slug taken

Dynamic product listings

Base path: /admin/dynamic-product-listings.

GET /admin/dynamic-product-listings — List listings

Required permission: dynamicProductListing: read. Standard QueryDto plus optional groupId to scope to one group.

Query params (additional)

NameTypeNotes
groupIdstring?Scope results to a single group

Response 200 — paginated envelope of DynamicProductListingResponse[].


GET /admin/dynamic-product-listings/available-filters — Get available facet catalog

Required permission: dynamicProductListing: read. Passes through to the product search engine and returns the full facet catalog (brands, ingredients, attributes) that can be used to build a ListingFilters spec. Useful for populating filter-builder UI dropdowns.

Envelope note: this is a product-search passthrough — the response format is the product-search engine's native envelope, not the standard ResponseInterceptor wrapper.

Response 200 — product-search passthrough envelope (facets only).


POST /admin/dynamic-product-listings/preview — Preview unsaved filters

Required permission: dynamicProductListing: read. Runs a ListingFilters spec against the search engine without saving it, returning matching products and facets. Useful for live previewing a filter spec before committing it.

Envelope note: this is a product-search passthrough — the response format is the product-search engine's native envelope (products + facets), not the standard ResponseInterceptor wrapper.

Body

{
  "filters": {
    "brands": ["acme"],
    "minPrice": 10000,
    "maxPrice": 50000,
    "inStock": true,
    "sortBy": "price-asc"
  },
  "page": 1,
  "limit": 20
}
FieldTypeConstraints
filtersListingFiltersSee ListingFilters
pageint?Default 1
limitint?Default 20

Response 200 — product-search passthrough envelope (products + facets).

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod

PATCH /admin/dynamic-product-listings/reorder — Bulk reorder within a group

Required permission: dynamicProductListing: update. Reapplies order values for the supplied listing ids within a group.

Body

{
  "groupId": "01J9aaaa...",
  "items": [
    { "listingId": "01J9xxxx...", "order": 0 },
    { "listingId": "01J9yyyy...", "order": 1 },
    { "listingId": "01J9zzzz...", "order": 2 }
  ]
}
FieldTypeConstraints
groupIdstringTarget group
items[].listingIdstringRequired
items[].orderint>= 0
itemsarrayMin 1 entry

Response 200 — reordered DynamicProductListingResponse[] in their new order.

Errors

StatusCodeWhen
400VALIDATION_ERRORMissing fields, empty items, etc.
404NOT_FOUNDGroup not found

GET /admin/dynamic-product-listings/:id — Get one listing

Required permission: dynamicProductListing: read.

Response 200DynamicProductListingResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/dynamic-product-listings — Create a listing

Required permission: dynamicProductListing: create.

Body

{
  "title": "Budget Supplements",
  "slug": "budget-supplements",
  "groupId": "01J9aaaa...",
  "filters": {
    "categories": ["supplements"],
    "maxPrice": 50000,
    "inStock": true,
    "sortBy": "price-asc"
  },
  "image": "listings/budget-supplements.jpg",
  "subtitle": "Great value picks",
  "description": "Hand-curated affordable supplements under ₹500.",
  "order": 0
}
FieldTypeConstraints
titlestring1..255
slugstring1..255, lowercase alnum + hyphens
groupIdstring | null?Optional parent group
filtersListingFilters | null?See ListingFilters
imagestring | null?Storage key
subtitlestring | null?Short caption
descriptionstring | null?Long description
orderint?>= 0. Default 0
metadataRecord<string, unknown> | null?Free-form

Response 201DynamicProductListingResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
409UNIQUE_VIOLATIONslug already in use

PUT /admin/dynamic-product-listings/:id — Update a listing

Required permission: dynamicProductListing: update. Partial body (same fields as create).

Response 200DynamicProductListingResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDUnknown id
409UNIQUE_VIOLATIONslug taken by another listing

DELETE /admin/dynamic-product-listings/:id — Delete a listing

Required permission: dynamicProductListing: delete. Hard delete.

Response 204 No Content.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/dynamic-product-listings/:id/duplicate — Duplicate a listing

Required permission: dynamicProductListing: create. Clones the listing with a new slug and optional new title. All fields (filters, image, subtitle, description, order, metadata) are copied from the source.

Body

{ "slug": "budget-supplements-v2", "title": "Budget Supplements (v2)" }
FieldTypeConstraints
slugstring1..255, lowercase alnum + hyphens; must be unique
titlestring?Optional override; defaults to source title

Response 201DynamicProductListingResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDSource listing not found
409UNIQUE_VIOLATIONNew slug taken

  • admin-rbac — gates every endpoint via dynamicProductListingGroup:* / dynamicProductListing:*. See admin-rbac.md.
  • store/dynamic-product-listing — unauthenticated storefront read of groups and listings by slug, with live product resolution.
  • search — the product-search engine that backs the /preview and /available-filters passthroughs.

On this page