> ## Documentation Index
> Fetch the complete documentation index at: https://docs.molin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Search API

> Query your Molin-indexed catalog from your own storefront code, using the same public read-only endpoints the search box calls.

The Search API returns products from the catalog Molin already indexed for your chatbot. Text queries combine semantic intent with keyword matching. It powers [`<molin-shop-ai-search>`](/home/general/elements), and you can call it directly when you want to build your own search or category UI.

All three endpoints are public, read-only, and need no API key: the widget ID identifies the catalog, and shoppers call them straight from the browser.

<Note>
  Call `widget.molin.ai`, never `molin.ai`. Keeping every widget request on one origin means you don't have to add another origin to your [Content Security
  Policy](/home/general/content-security-policy).
</Note>

## `GET /v1/search`

```bash theme={null}
curl "https://widget.molin.ai/v1/search?widgetId=abc12345&q=winter%20jacket&limit=12"
```

<ParamField query="widgetId" type="string" required>
  Your widget ID, which selects the catalog to search.
</ParamField>

<ParamField query="q" type="string" optional>
  The search text. Omit it to list the catalog, which is what a category page needs.
</ParamField>

<ParamField query="filters" type="JSON array" optional default="[]">
  Up to 10 product-attribute filters. Each filter has a `key` and `value`, and the whole array is JSON-encoded. Use the stable keys returned by `/v1/search/facets`: `brand`, `category`, `color`,
  `size`, `material`, or `gender`. Existing integrations can continue to send a raw feed attribute key.
</ParamField>

<ParamField query="priceMin" type="number" optional>
  Lowest price to include.
</ParamField>

<ParamField query="priceMax" type="number" optional>
  Highest price to include.
</ParamField>

<ParamField query="currency" type="string" optional>
  Three-letter currency code for the price filters.
</ParamField>

<ParamField query="sort" type="'newest' | 'oldest' | 'cheapest' | 'priciest'" optional>
  Sort order. Omit it to sort by relevance.
</ParamField>

<ParamField query="limit" type="number" optional default="20">
  Products per page, from 1 through 60.
</ParamField>

<ParamField query="offset" type="number" optional default="0">
  Products to skip, up to 500. The ranker scores `limit + offset` candidates, so deep paging is bounded deliberately.
</ParamField>

### Response

```json theme={null}
{
  "results": [
    {
      "url": "https://store.example/products/alpine-winter-jacket",
      "title": "Alpine Winter Jacket",
      "price": "199.00 EUR",
      "originalPrice": "249.00 EUR",
      "priceAmount": 199,
      "image": "https://store.example/cdn/jacket.jpg",
      "sku": "AWJ-001",
      "gtin": "5901234123457",
      "storefrontProductId": "8123456789",
      "stockQuantity": 14
    }
  ],
  "total": 1,
  "exhausted": true
}
```

<ResponseField name="results" type="array">
  Matching products in ranked order. `originalPrice` is empty when the source didn't provide a price before the discount. `priceAmount` and `stockQuantity` are `null` when the source data didn't
  provide them.
</ResponseField>

<ResponseField name="total" type="number">
  For a text query, the number of ranked candidates. For a filter-only listing, the full number of matches before `limit` and `offset`.
</ResponseField>

<ResponseField name="exhausted" type="boolean">
  `true` when this page reached the end of the matches, so there is no next page to request.
</ResponseField>

## `GET /v1/search/facets`

Returns the stable filters available for one catalog. Molin groups equivalent source fields such as `Color`, `Colour`, and `Szín` under `color` without renaming or changing the feed data. Values stay exactly as the feed supplied them.

```bash theme={null}
curl "https://widget.molin.ai/v1/search/facets?widgetId=abc12345"
```

<ParamField query="widgetId" type="string" required>
  Your widget ID.
</ParamField>

### Response

```json theme={null}
{
  "facets": [
    { "key": "color", "values": ["Black", "Fekete", "Blue"] },
    { "key": "size", "values": ["S", "M", "L"] }
  ]
}
```

<ResponseField name="facets" type="array">
  Available `brand`, `category`, `color`, `size`, `material`, and `gender` filters. Each entry contains the most common values from the catalog. Store-specific fields outside this reviewed list are
  not exposed here.
</ResponseField>

## `GET /v1/search/suggest`

Returns the whole suggestion set for a widget in one response. Fetch it once, then filter it in the browser as the shopper types, rather than requesting per keystroke.

```bash theme={null}
curl "https://widget.molin.ai/v1/search/suggest?widgetId=abc12345"
```

<ParamField query="widgetId" type="string" required>
  Your widget ID.
</ParamField>

### Response

```json theme={null}
{
  "queries": ["winter jacket", "ski gloves"],
  "products": [
    {
      "title": "Alpine Winter Jacket",
      "url": "https://store.example/products/alpine-winter-jacket",
      "image": "https://store.example/cdn/jacket.jpg",
      "price": "199.00 EUR",
      "originalPrice": "249.00 EUR"
    }
  ]
}
```

<ResponseField name="queries" type="string[]">
  Searches other shoppers on your store ran and got results for. Searches that always came back empty are excluded, so a suggestion never leads to a dead end. A search only appears once at least two
  different shoppers ran it.
</ResponseField>

<ResponseField name="products" type="array">
  Newest products with their image and prices, as a starting point for a store with no search history yet. `originalPrice` is empty when no discount price was supplied.
</ResponseField>

## Caching and limits

Search responses are cacheable for 60 seconds and revalidate in the background for a further 5 minutes. Facets and suggestions are cached for 5 minutes and can still be served for a week while they refresh in the background, because they move slowly and slightly stale data is harmless.

Search is rate limited per IP. Repeated identical searches are usually served from the edge cache without reaching our worker at all.

## Errors

All three endpoints answer `400` for a missing or unknown widget ID and for any parameter outside the ranges described earlier, with no body. Treat a rejection as a bug in the request rather than something to retry.
