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

# Products, categories & shop

> Read the catalog: products, search, categories and the shop's branding.

<Info>
  This page is the curated reference. The complete, generated type reference lives at [/kit/api/core](/kit/api/core) and is regenerated from the published typings.
</Info>

Everything here is read-only and needs the `products:read` scope, except `shop.get()`, which needs `checkout:read`. Only **visible** products come back; cost prices, suppliers and internal notes are never exposed to a publishable key. The data shapes are on [Types](/kit/reference/types).

Every method takes an optional `signal` (an `AbortSignal`) to cancel the request on a route change or unmount.

**Currency.** Every product read carries the client's currency as `?currency=` (see [Currencies](/kit/concepts/currencies)), and each read also takes a per-call `currency`: a code prices that one read in it, `null` reads the shop's own prices. Every product states what its prices came back in as `product.currency`; format with that. A well-formed code the shop does not offer silently falls back to the shop's currency; a malformed one throws a `ShopkitConfigError` before the request.

## products.list

```ts theme={null}
shopkit.products.list(params?: ProductListParams): Promise<Page<Product>>
```

The catalog in the merchant's own order. Accepts only paging. For anything a shopper navigates (a category page, a search box, a sorted grid) use `products.search` (below) instead: `list()` filters hidden products after paging, so its pages can come back shorter than `limit`.

<ParamField body="limit" type="number" default="50">Page size, 1 to 200. Above 200 is capped; `0` or a negative value is a 400.</ParamField>
<ParamField body="cursor" type="string">The `next_cursor` from the previous page.</ParamField>
<ParamField body="storefrontId" type="string | null">Price this read for a campaign storefront. Omitted means the client's binding; `null` means the shop's ordinary prices. See [Campaign storefronts](/kit/concepts/campaign-storefronts).</ParamField>
<ParamField body="currency" type="string | null">Price this read in another currency. Omitted means the client's currency; `null` means the shop's own. See [Currencies](/kit/concepts/currencies).</ParamField>

<ParamField body="signal" type="AbortSignal" />

<ResponseField name="data" type="Product[]" />

<ResponseField name="has_more" type="boolean" />

<ResponseField name="next_cursor" type="string | null">Feed it back as `cursor`.</ResponseField>

## products.search

```ts theme={null}
shopkit.products.search(params?: ProductSearchParams): Promise<Page<Product>>
```

Filtered and sorted in the database, with visibility applied in the query so pages arrive full.

<ParamField body="search" type="string">
  Free text, matched against product name, SKU, GTIN, variant SKU and GTIN, and option values. Words are AND-ed and order-independent (`"merino crew"` finds "Crew neck, merino"). Only the first 10 words count.
</ParamField>

<ParamField body="sortBy" type="'name' | 'price' | 'createdAt'" default="createdAt">`createdAt` is the catalog order.</ParamField>

<ParamField body="sortOrder" type="'asc' | 'desc'" default="desc" />

<ParamField body="minPrice" type="number">Inclusive lower bound in **minor units** (`20000` is 200.00).</ParamField>
<ParamField body="maxPrice" type="number">Inclusive upper bound in minor units. An inverted range (`minPrice` above `maxPrice`) is a 400.</ParamField>
<ParamField body="categoryId" type="string | number">`"cat_12"` or `12`. **Direct membership only**: child categories are not walked.</ParamField>
<ParamField body="limit" type="number" default="50">1 to 200.</ParamField>

<ParamField body="cursor" type="string" />

<ParamField body="storefrontId" type="string | null">Same as on `list()`.</ParamField>
<ParamField body="currency" type="string | null">Same as on `list()`. The results are converted; the price bounds and sort are not (see below).</ParamField>

<ParamField body="signal" type="AbortSignal" />

<Warning>
  Price bounds and `sortBy: "price"` match the **undiscounted** list price, **in the shop's own currency**, even when the results come back converted into another one. Discounts and the conversion are applied per page after the query, so a product can come back priced outside the bounds you asked for. Label price filters in `shop.currency`.
</Warning>

What `search()` does not do: return facet counts, filter by an option value across the catalog ("everything in black"), or match slugs.

```ts theme={null}
const page = await shopkit.products.search({
  search: "merino",
  categoryId: "cat_12",
  sortBy: "price",
  sortOrder: "asc",
  minPrice: 20000,
  maxPrice: 90000,
  limit: 24,
})
```

## products.get

```ts theme={null}
shopkit.products.get(
  productId: string | number,
  options?: { storefrontId?: string | null; currency?: string | null; signal?: AbortSignal },
): Promise<Product | null>
```

One product by id, `"prod_27"` or `27`. Resolves to `null` when the product does not exist or is not visible: a missing product is a normal answer, not an exception.

Only this method returns `relatedProducts` on the product.

## products.getBySlug

```ts theme={null}
shopkit.products.getBySlug(
  slug: string,
  params?: { pageSize?: number; maxPages?: number; storefrontId?: string | null; currency?: string | null; signal?: AbortSignal },
): Promise<Product | null>
```

The API has no slug filter, so this **pages through the catalog** until it finds a match. The currency is resolved once, so every page of the walk is priced in the same one. Matching is case-insensitive and ignores surrounding slashes, so a route param works as is. `maxPages` defaults to 200.

<Tip>
  On a large catalog, cache the lookup per slug, or build a slug-to-id map at deploy time from `listAll()` and call `get()`, which is a single request.
</Tip>

## products.listAll

```ts theme={null}
shopkit.products.listAll(
  params?: { pageSize?: number; storefrontId?: string | null; currency?: string | null; signal?: AbortSignal },
): Promise<Product[]>
```

Every product, for static params and sitemaps. Bounded at 200 pages. Do not call it while rendering a request.

## categories.list

```ts theme={null}
shopkit.categories.list(params?: CategoryListParams): Promise<Page<Category>>
```

<ParamField body="root" type="boolean">Only top-level categories.</ParamField>
<ParamField body="parentId" type="string">Children of one category, one level down.</ParamField>

<ParamField body="search" type="string" />

<ParamField body="limit" type="number" />

<ParamField body="cursor" type="string" />

<ParamField body="signal" type="AbortSignal" />

Categories sit behind `products:read`. A category page is `products.search({ categoryId })`. To include products that live only in child categories, walk the tree with `categories.list({ parentId })` and query each.

## categories.get

```ts theme={null}
shopkit.categories.get(categoryId: string | number, options?: { signal?: AbortSignal }): Promise<Category | null>
```

`null` when the category does not exist.

## shop.get

```ts theme={null}
shopkit.shop.get(options?: { signal?: AbortSignal }): Promise<Shop>
```

Name, logo, brand colour, language, terms URL, the currencies the shop offers and its tracking ids: what a storefront shell needs before it renders. Requires `checkout:read`, because it is served by the checkout's shop endpoint, the only shop surface a publishable key can reach.

```ts theme={null}
const shop = await shopkit.shop.get()
shop.name
shop.brand.logo
shop.brand.color.primary // hex, or null
shop.language            // "sv"
shop.currency            // "SEK": the shop's own (base) currency
shop.currencies          // [{ code: "SEK", mode: "base", rate: 1 }, { code: "EUR", mode: "charge", rate: 0.0871 }]
shop.tracking            // { ga4MeasurementId, gtmContainerId, metaPixelId, metaCapiEnabled }
```

<ResponseField name="currency" type="string | null">The shop's own currency, upper-case ISO 4217. Absent on a platform older than the field.</ResponseField>
<ResponseField name="currencies" type="ShopCurrency[]">Every currency a shopper may browse in, base first: `{ code, mode: "base" | "display" | "charge", rate }`. Only the base entry when the merchant's currency converter is off. See [Currencies](/kit/concepts/currencies).</ResponseField>
<ResponseField name="tracking" type="ShopTracking | null">The merchant's GA4 measurement id, GTM container id and Meta pixel id (validated by the platform; `null` when unset), and whether the platform sends Meta Conversions API events. What the kit's analytics loads by default. See [Consent and analytics](/kit/concepts/consent-and-analytics).</ResponseField>

The full shape is on [Types](/kit/reference/types#shop).

## Shop cache

```ts theme={null}
import { getShopPromise, clearShopCache } from "@quickbutik/kit"

getShopPromise(client)   // Promise<Shop>: one shop.get() per client, shared
clearShopCache(client)   // forget it, so the next read goes to the API
```

The shop does not change while a page is open, and every currency switcher, `useCurrency()` and `<qb-currency-select>` needs it. This cache gives them one request between them. A rejected read is evicted, so the next caller retries.

## Product lookup cache

```ts theme={null}
import { getProductPromise, clearProductCache } from "@quickbutik/kit"

getProductPromise(client, { slug: "cotton-tee" })     // Promise<Product | null>, cached per client
getProductPromise(client, { id: "prod_27", storefrontId: "sf_…" })
getProductPromise(client, { slug: "cotton-tee", currency: "EUR" })
clearProductCache(client)                             // everything for this client
clearProductCache(client, { slug: "cotton-tee" })     // one entry
```

The promise cache behind `<ProductProvider slug|id>` and `<qb-product>`. Two lookups of the same product share one request. Entries are keyed on the currency (the lookup's own, else the client's current one) and the campaign, so a currency switch never hands back a promise priced in the old currency. Call `clearProductCache` after a revalidation in a long-lived browser session.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.