Supercommerce API Docs
Store API

Search Module — Storefront

HTTP surface for the storefront product search — Typesense-backed faceted search and autocomplete suggestions. Mirrors the beautybarn response shape so the catalog/listing screens…

HTTP surface for the storefront product search — Typesense-backed faceted search and autocomplete suggestions. Mirrors the beautybarn response shape so the catalog/listing screens have a near drop-in surface during the re-platform.

Source: api-modules/search/src/controllers/store-product-search.controller.ts.

The admin reindex/backfill surface lives in docs/separated/admin/search.md.


Conventions

Authentication

EndpointAuth
GET /store/product-searchnone (public)
GET /store/product-search/suggestionsnone (public)

Response envelope

The search and suggestions endpoints embed their own data key so the wire envelope is the standard { data, metadata?, message, statusCode }:

{
  "data": { /* products + facets, or suggestions + products */ },
  "metadata": { /* on /store/product-search only */ },
  "message": "Success",
  "statusCode": 200
}

The metadata uses the named-pagination shape (total / items / perPage / currentPage / lastPage) rather than the offset-based total / limit / offset / hasMore shape — search is page-based.

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR (malformed attributes JSON, bad price coercion)
500INTERNAL_SERVER_ERROR, SEARCH_UPSTREAM_ERROR

Currency

All variant price fields (price, specialPrice, originalPrice, currentPrice, specialPriceActive, priceStart, priceEnd, minPrice/maxPrice query) are integer subunits (paise / cents / eurocents).


Domain types

ProductSearchVariant

type ProductSearchVariant = {
  id: string;
  sku: string | null;
  price: number | null;
  specialPrice: number | null;
  specialPriceStartDate: string | null;     // ISO
  specialPriceEndDate: string | null;       // ISO
  inventoryQuantity: number;
  minQuantityPerCart: number | null;
  maxQuantityPerCart: number | null;
  thumbnail: string | null;
  images: string[];

  /** Computed at response time from the fields above. */
  originalPrice: number | null;             // = price
  currentPrice: number | null;              // = specialPriceActive ?? price
  specialPriceActive: number | null;        // specialPrice if window currently includes now, else null
};

ProductSearchProduct

type ProductSearchProduct = {
  id: string;
  title: string;
  subtitle: string | null;
  description: string | null;
  slug: string;                              // (beautybarn called this "handle")
  thumbnail: string | null;
  images: string[];
  /** min/max of currentPrice across in-stock variants; null when no variant is priced. */
  priceStart: number | null;
  priceEnd: number | null;
  brand: { id: string; slug: string; name: string } | null;
  inStock: boolean;
  hasActiveSpecial: boolean;
  variants: ProductSearchVariant[];
};

Facets

type ProductSearchBrandFacet = {
  id: string;
  slug: string;
  name: string;
  productCount: number;
};

type ProductSearchIngredientFacet = {
  id: string;
  slug: string;
  name: string;
  productCount: number;
};

type ProductSearchAttributeFacet = {
  code: string;
  title: string;
  values: Array<{ value: string; productCount: number }>;
};

Endpoints

Returns paginated products plus three facet blocks (brands, ingredients, attributes) computed against the same filtered set so client-side facet UI counts stay accurate.

Query

NameTypeDefaultNotes
qstring?Trimmed, max 200 chars. Full-text query against title + description.
brandscsv string?Comma-separated brand slugs (or ids — depends on the index field).
categoriescsv string?Comma-separated category slugs/ids.
tagscsv string?Comma-separated tag slugs/ids.
ingredientscsv string?Comma-separated ingredient slugs/ids.
skuscsv string?Comma-separated SKUs (variant-level).
attributesJSON-encoded string?{ "<attribute-code>": "value" | string[] }. Malformed JSON or non-string values returns 400.
minPriceint?Subunit lower bound (inclusive).
maxPriceint?Subunit upper bound (inclusive).
inStock"true" | "false"?Coerced to boolean. When omitted, no filter is applied.
hasActiveSpecial"true" | "false"?Coerced to boolean.
pageint11..1000
limitintDEFAULT_SEARCH_LIMIT1..MAX_SEARCH_LIMIT
sortByenum?relevance (implicit)One of relevance / price-asc / price-desc / new / best-selling / inventory-high / inventory-low

Response 200

{
  "data": {
    "products": [
      {
        "id": "01J9...",
        "title": "Hydrating Toner",
        "subtitle": "200ml",
        "description": "...",
        "slug": "hydrating-toner",
        "thumbnail": "https://cdn.example/p/hydrating-toner.jpg",
        "images": ["https://cdn.example/p/hydrating-toner-2.jpg"],
        "priceStart": 49900,
        "priceEnd": 79900,
        "brand": { "id": "01J9...", "slug": "skin-co", "name": "Skin Co" },
        "inStock": true,
        "hasActiveSpecial": false,
        "variants": [ /* ProductSearchVariant[] */ ]
      }
    ],
    "brands": [ { "id": "01J9...", "slug": "skin-co", "name": "Skin Co", "productCount": 12 } ],
    "ingredients": [ { "id": "01J9...", "slug": "niacinamide", "name": "Niacinamide", "productCount": 8 } ],
    "attributes": [
      { "code": "size", "title": "Size", "values": [ { "value": "200ml", "productCount": 5 } ] }
    ]
  },
  "metadata": {
    "total": 42,
    "items": 20,
    "perPage": 20,
    "currentPage": 1,
    "lastPage": 3
  },
  "message": "Success",
  "statusCode": 200
}

GET /store/product-search/suggestions — Autocomplete

Returns distinct title suggestions plus a small set of matching products for an inline search dropdown.

Query

NameTypeDefaultNotes
qstring— requiredTrimmed, 2..100 chars
limitintDEFAULT_SUGGESTIONS_LIMIT1..20

Response 200

{
  "data": {
    "suggestions": ["Hydrating Toner", "Hydrating Serum"],
    "products": [ /* ProductSearchProduct[] */ ]
  },
  "message": "Success",
  "statusCode": 200
}

Errors

StatusCodeWhen
400VALIDATION_ERRORq < 2 chars

  • catalog — products and variants returned here are reindexed from the catalog module's writes.
  • inventoryinStock / inventoryQuantity reflect the inventory module's current view (with a small replication lag — the search index is eventually consistent).
  • product-attributeattributes facet values come from the product-attribute module.

On this page