Supercommerce API Docs
Admin API

Catalog Module — Admin

HTTP surface for platform-admin reads of products + variants and full CRUD over the platform-wide taxonomy (brands, categories, tags, ingredients), with a vendor-request approval…

HTTP surface for platform-admin reads of products + variants and full CRUD over the platform-wide taxonomy (brands, categories, tags, ingredients), with a vendor-request approval workflow per taxonomy. Vendor-side product/variant/tab CRUD and the public storefront reads live in the same module but are out of scope here — see vendor/catalog.md and store/catalog.md.

Source: api-modules/catalog/src/controllers/admin-product.controller.ts, api-modules/catalog/src/controllers/admin-variant.controller.ts, api-modules/catalog/src/controllers/admin-brand.controller.ts, api-modules/catalog/src/controllers/admin-category.controller.ts, api-modules/catalog/src/controllers/admin-tag.controller.ts, api-modules/catalog/src/controllers/admin-ingredient.controller.ts.

HSN classification lives on product_variant, not product — variants in the same family can have different HSN codes. The admin product detail endpoint surfaces both the informational product-level hsCode and the canonical per-variant hsnCode.


Conventions

Authentication

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

Endpoint groupPermission
GET /admin/products, GET /admin/products/:id/detail, GET /admin/variantsproduct: view
GET /admin/catalog/brands/**brand: read
POST /admin/catalog/brandsbrand: create
PUT /admin/catalog/brands/:id, POST /admin/catalog/brands/:id/restorebrand: update
DELETE /admin/catalog/brands/:idbrand: delete
POST /admin/catalog/brands/requests/:id/approve|rejectbrand: approve
/admin/catalog/categories/**category:* (same shape as brands)
/admin/catalog/tags/**tag:* (same shape)
/admin/catalog/ingredients/**ingredient:* (same shape)

Response envelope

Successful responses are wrapped by ResponseInterceptor:

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

Some admin list endpoints return a picker envelope (items[] + pinned[]) — see Picker responses.

Error envelope

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

Lifecycle / soft-delete

Brands, categories, tags, ingredients, products, and variants all soft-delete via deletedAt. DELETE flips deletedAt; POST /:id/restore clears it. Slugs are unique among non-deleted rows — soft-deleting frees the slug for re-use; restoring may then fail with UNIQUE_VIOLATION if the slug has been taken in the meantime.

Currency

Variant price / specialPrice fields are integer subunits (paise / cents). Single currency per tenant.


Domain types

Taxonomy item — CatalogItemResponse

Shared by brand / category / tag / ingredient.

type CatalogItemResponse = {
  id: string;
  title: string;
  description: string | null;
  slug: string;                            // lowercase alphanumeric + hyphens
  image: string | null;
  metadata: Record<string, unknown> | null;
  isActive: boolean;
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
};

AdminCatalogItemResponse

Admin list-only variant adds an aggregated bannerCount from the polymorphic banner table — so the UI can warn before deleting a brand/category that still has banners attached.

type AdminCatalogItemResponse = CatalogItemResponse & { bannerCount: number };

AdminVariantListItem (variant picker)

type AdminVariantListItem = {
  id: string;
  productId: string;
  productTitle: string;
  sku: string | null;
  thumbnail: string | null;
  price: number | null;                    // subunits
};

Catalog request

Vendor-submitted taxonomy proposals.

type CatalogRequest = CatalogItemResponse & {
  status: "pending" | "approved" | "rejected";
  vendorId: string;
  requestedByUserId: string;
  rejectionReason: string | null;
  approvedAt: string | null;
  rejectedAt: string | null;
  resultingItemId: string | null;          // populated on approval
};

type CategoryRequest = CatalogRequest & { parentId: string | null };

Picker responses

GET /admin/catalog/brands, GET /admin/catalog/categories, GET /admin/catalog/tags, GET /admin/catalog/ingredients, and GET /admin/variants return a picker envelope that pairs the regular paginated items[] with a pinned[] list of always-shown rows. Use the selectedIds=<csv> query param to pre-pin currently-selected items so the picker never has to manually search to render an already-applied selection.

{
  "data": {
    "items":  [ /* page of unpinned rows */ ],
    "pinned": [ /* full rows for every id in selectedIds */ ]
  },
  "metadata": { "total": 412, "items": 20, "perPage": 20, "currentPage": 1, "lastPage": 21 }
}

When selectedIds is absent, pinned is [].


Products

Base path: /admin/products. Reads only — vendors own product writes.

GET /admin/products — List products

Required permission: product: view. Cross-vendor list with filters and full-text search.

Query

NameTypeDefaultNotes
qstring?Free-text search (trimmed, min 1)
vendorIdstring?Filter to one vendor
brandIdstring?Filter to one brand
primaryCategoryIdstring?Filter on the primary category
categoryIdstring?Match any of the product's categories
tagIdstring?Match any of the product's tags
ingredientIdstring?Match any ingredient
status"draft" | "active" | "archived"?Product status
visibility"public" | "private"?Product visibility
createdFrom, createdToISO datetime?createdAt range
publishedFrom, publishedToISO datetime?publishedAt range
sortBy"createdAt" | "updatedAt" | "publishedAt" | "title" | "vendorName""updatedAt"
sortDirection"asc" | "desc""desc"
limitint1001..500
offsetint0>= 0
selectedIdscsv or string[]?Pre-pin rows for the picker envelope

Response 200 — picker envelope. Items shape is the service-defined admin product list row (vendor info, brand, status, etc.). Detail-shaped via GET /:id/detail.


GET /admin/products/:id/detail — Product detail

Required permission: product: view. Returns the product with vendor info, options, variants, tabs, and full taxonomy joins. Same shape the vendor product detail endpoint returns.

Response 200ProductDetail (vendor info + options + variants + tabs + categories/tags/ingredients arrays).

Errors

StatusCodeWhen
404NOT_FOUNDUnknown product id

Variants — cross-product picker

GET /admin/variants — Variant picker

Required permission: product: view. Flat list across every vendor's products. Standard QueryDto (page / limit / search / selectedIds). Search runs across product title and variant SKU.

Response 200 — picker envelope of AdminVariantListItem[]. See Picker responses.


Taxonomy CRUD (brand / category / tag / ingredient)

The four taxonomies share an almost-identical CRUD surface. Brand is the canonical example; per-taxonomy variations are listed at the end.

Base paths:

TaxonomyBase path
Brand/admin/catalog/brands
Category/admin/catalog/categories
Tag/admin/catalog/tags
Ingredient/admin/catalog/ingredients

Per-taxonomy permissions:

ResourceReadCreateUpdateDeleteApprove
brandbrand: readbrand: createbrand: updatebrand: deletebrand: approve
categorycategory: readcategory: createcategory: updatecategory: deletecategory: approve
tagtag: readtag: createtag: updatetag: deletetag: approve
ingredientingredient: readingredient: createingredient: updateingredient: deleteingredient: approve

GET /admin/catalog/<taxonomy> — List

Required permission: read. Standard QueryDto (page / limit / search / filters[] / selectedIds).

Response 200 — picker envelope of AdminCatalogItemResponse[] (items + pinned, with bannerCount).


GET /admin/catalog/categories/tree — Category tree (category only)

Required permission: category: read. Returns the full nested category tree (parent → children).

Response 200 — nested CatalogItemResponse[].


GET /admin/catalog/<taxonomy>/:id — Get one

Required permission: read.

Response 200CatalogItemResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/catalog/<taxonomy> — Create

Required permission: create.

Body (brand/tag/ingredient)

{
  "title": "L'Oréal",
  "description": "...",                        // optional, max 2000
  "slug": "loreal",
  "image": "brands/loreal.png",                // optional
  "metadata": { "country": "FR" },             // optional
  "isActive": true
}
FieldTypeConstraints
titlestring1..255
descriptionstring | null?max 2000
slugstring1..255, lowercase alnum + hyphens (/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
imagestring | null?Storage key (free-form)
metadataRecord<string, unknown> | null?Free-form
isActivebooleanDefault true

Category-only extras

FieldTypeNotes
parentIdstring | null?Parent category (omit/null for a root)
sortOrderint>= 0. Default 0

Response 201CatalogItemResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
409UNIQUE_VIOLATIONSlug already in use by a non-deleted row

PUT /admin/catalog/<taxonomy>/:id — Update

Required permission: update. Partial — every field is optional.

Response 200CatalogItemResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
409UNIQUE_VIOLATIONNew slug collides with a non-deleted row

DELETE /admin/catalog/<taxonomy>/:id — Soft-delete

Required permission: delete.

Response 200 — the deleted CatalogItemResponse.


POST /admin/catalog/<taxonomy>/:id/restore — Restore

Required permission: update.

Response 200 — restored CatalogItemResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
409UNIQUE_VIOLATIONOriginal slug has been re-used while soft-deleted

Vendor catalog request approvals

Vendors submit new-taxonomy proposals via the vendor-side endpoints; admins review them through these endpoints.

Base path: /admin/catalog/<taxonomy>/requests. Same taxonomy table above.


GET /admin/catalog/<taxonomy>/requests/all — List requests

Required permission: read. Standard QueryDto.

Response 200 — paginated envelope of CatalogRequest[].


GET /admin/catalog/<taxonomy>/requests/:id — Get a request

Required permission: read.

Response 200CatalogRequest.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/catalog/<taxonomy>/requests/:id/approve — Approve

Required permission: approve. Transitions the request to approved, creates the canonical taxonomy row from the request's fields, and stamps approvedAt + resultingItemId. The request session's user is recorded as the approver.

Response 200 — the approved CatalogRequest (with resultingItemId populated).

Errors

StatusCodeWhen
404NOT_FOUNDRequest not found or not in pending

POST /admin/catalog/<taxonomy>/requests/:id/reject — Reject

Required permission: approve.

Body

{ "reason": "Duplicate of brand 'Existing Brand'" }
FieldTypeConstraints
reasonstring1..2000

Response 200 — the rejected CatalogRequest.

Errors

StatusCodeWhen
400VALIDATION_ERROREmpty / oversized reason
404NOT_FOUNDRequest not found or not in pending

Domain events

Emitted via EventEmitter2 — consumed by the search reindexer and the cart price-cache invalidator.

EventFired when
catalog.brand.created / .updated / .deletedAdmin brand writes
catalog.category.*, .tag.*, .ingredient.*Admin taxonomy writes
catalog.request.approved / .rejectedAdmin approval flow

  • admin-rbac — gates every endpoint via the per-taxonomy permission strings above. See admin-rbac.md.
  • banner — banner rows reference the four taxonomy tables; bannerCount on the admin list comes from this join. See banner.md.
  • search — consumes the catalog.* events to keep the product index in sync.
  • order — order line items snapshot hsnCode from the variant. See order.md.

On this page