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

# Installation

> Install and configure the Layers SDK in your project, including package manager setup, client initialization, and TypeScript type generation.

## Installation

```bash theme={null}
npm install @commerce-blocks/sdk
```

### Loading from a CDN

The SDK ships as a pure ES module with no Node-only dependencies, so it works in the browser with no build step. Use this when you're integrating directly into a Shopify theme, a static HTML page, or any environment where you don't run `npm install`.

#### unpkg (recommended)

[unpkg](https://unpkg.com) serves the published npm package directly. Add the `?module` query string and unpkg rewrites the SDK's `@preact/signals-core` import into a sibling unpkg URL, so the browser resolves the entire module graph without a bundler.

```html theme={null}
<script type="module">
  import { createClient } from 'https://unpkg.com/@commerce-blocks/sdk?module'

  const { data: client, error } = createClient({
    token: 'your-token',
    sorts: [{ name: 'Featured', code: 'featured' }],
    facets: [{ name: 'Color', code: 'options.color' }],
  })

  if (error) {
    console.error('Layers init failed:', error.message)
  } else {
    window.layers = client
  }
</script>
```

<Tip>
  Pin the version explicitly in production so a new release never silently changes behavior on your storefront. unpkg accepts a major (`@2`), minor (`@2.0`), or exact (`@2.0.3`) tag — for example, `https://unpkg.com/@commerce-blocks/sdk@2?module`. See the [SDK changelog](/developers/sdk/changelog) for the latest version.
</Tip>

#### esm.sh

[esm.sh](https://esm.sh) is an alternative ESM CDN that pre-bundles the SDK and its dependency into a single file.

```html theme={null}
<script type="module">
  import { createClient } from 'https://esm.sh/@commerce-blocks/sdk@2'
</script>
```

#### jsDelivr

[jsDelivr](https://www.jsdelivr.com) mirrors npm with its own ESM transform. Use the `+esm` suffix to get a browser-compatible bundle.

```html theme={null}
<script type="module">
  import { createClient } from 'https://cdn.jsdelivr.net/npm/@commerce-blocks/sdk@2/+esm'
</script>
```

#### Import maps

If you load multiple modules from a CDN and want to keep imports short, declare an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) once and use bare specifiers everywhere else:

```html theme={null}
<script type="importmap">
{
  "imports": {
    "@commerce-blocks/sdk": "https://unpkg.com/@commerce-blocks/sdk@2?module"
  }
}
</script>

<script type="module">
  import { createClient, getClient } from '@commerce-blocks/sdk'
  // …
</script>
```

#### Reusing the client across modules

`createClient` registers the client in a singleton, so any later `<script type="module">` block can pull it back with `getClient`. This is the cleanest pattern for theme integrations where one snippet boots the SDK and many other sections consume it.

```html theme={null}
<!-- Boot once in theme.liquid -->
<script type="module">
  import { createClient } from 'https://unpkg.com/@commerce-blocks/sdk@2?module'

  createClient({
    token: '{{ shop.metafields.layers.embed_settings.value.storefrontApiToken }}',
    sorts: [{ name: 'Featured', code: 'featured' }],
    facets: [{ name: 'Color', code: 'options.color' }],
  })
</script>

<!-- Consume anywhere -->
<script type="module">
  import { getClient } from 'https://unpkg.com/@commerce-blocks/sdk@2?module'

  const { data: client } = getClient()
  const collection = client.collection({ handle: 'shirts' })
  await collection.execute()
</script>
```

<Tip>
  When pulling the SDK from a CDN, also add `<link rel="modulepreload" href="https://unpkg.com/@commerce-blocks/sdk@2?module" />` to your `<head>` so the browser starts fetching it during HTML parse instead of waiting for the first `<script type="module">` to execute.
</Tip>

## Configuration

### Required configuration

| Option   | Type      | Required | Description              |
| -------- | --------- | -------- | ------------------------ |
| `token`  | `string`  | Yes      | Layers API public token  |
| `sorts`  | `Sort[]`  | Yes      | Sort options (see below) |
| `facets` | `Facet[]` | Yes      | Facet fields (see below) |

**Sort:**

| Property | Type                           | Required | Description                                                 |
| -------- | ------------------------------ | -------- | ----------------------------------------------------------- |
| `name`   | `string`                       | Yes      | Display name shown to users                                 |
| `code`   | `string`                       | Yes      | Sort order code configured in Layers                        |
| `scope`  | `('search' \| 'collection')[]` | No       | Restrict to specific controllers. Omit to allow everywhere. |
| `order`  | `number`                       | No       | Display order (lower numbers appear first)                  |

**Facet:**

| Property | Type     | Required | Description                                                                 |
| -------- | -------- | -------- | --------------------------------------------------------------------------- |
| `name`   | `string` | Yes      | Display name shown to users                                                 |
| `code`   | `string` | Yes      | Attribute code in Layers (e.g. `options.color`, `vendor`, `variants.price`) |

<Note>
  Every `sorts[].code` and `facets[].code` must match a sort order or attribute that exists in your Layers dashboard. The server validates these on each request. If a value is wrong or stale, `execute()` returns an `ApiError` whose message reads `"The selected sort order code is invalid."` or `"The selected facets.N is invalid."` (where `N` is the zero-based index of the offending facet). Fix these by updating the `code` values passed to `createClient()` to match your dashboard configuration. See [Troubleshooting](#troubleshooting).
</Note>

### Optional configuration

| Option       | Type          | Description                                                                              |
| ------------ | ------------- | ---------------------------------------------------------------------------------------- |
| `attributes` | `string[]`    | Product attributes to fetch                                                              |
| `baseUrl`    | `string`      | Custom API URL                                                                           |
| `fetch`      | `CustomFetch` | Custom fetch implementation (SSR, testing)                                               |
| `context`    | `Context`     | Default market and shopper context applied to every controller (see [Context](#context)) |

### Context

Pass market and shopper context to personalize results across all controllers. Set it globally on the client config, per-query on `execute()`, or both — per-query context shallow-merges with and overrides the global context.

```typescript theme={null}
import { createClient } from '@commerce-blocks/sdk'

const { data: client } = createClient({
  token: 'your-token',
  sorts: [{ name: 'Featured', code: 'featured' }],
  facets: [{ name: 'Color', code: 'options.color' }],
  context: {
    market: 'US',
    geo: { country: 'US' },
    shoppingChannel: 'web',
  },
})

// Per-query override — merges with global context
const collection = client.collection({ handle: 'shirts' })
await collection.execute({
  context: { geo: { country: 'CA', province: 'ON' } },
})
// Effective context: { market: 'US', geo: { country: 'CA', province: 'ON' }, shoppingChannel: 'web' }
```

**`Context` fields:**

| Field               | Type                      | Description                                                      |
| ------------------- | ------------------------- | ---------------------------------------------------------------- |
| `geo`               | `GeoLocation`             | Geographic location: `{ country, province, city }`               |
| `market`            | `string`                  | Market identifier                                                |
| `productsInCart`    | `CartProduct[]`           | Products currently in the shopper's cart                         |
| `productsPurchased` | `CartProduct[]`           | Previously purchased products                                    |
| `priorSearches`     | `PriorSearch[]`           | Recent searches: `{ searchQuery, hadClick, hasResults }`         |
| `marketing`         | `Marketing`               | UTM-style attribution: `{ source, medium, campaign, term }`      |
| `customer`          | `CustomerContext`         | Customer profile: `{ signedIn, returning, numberOfOrders, ... }` |
| `shoppingChannel`   | `'web' \| 'app'`          | Shopping channel the request originates from                     |
| `custom`            | `Record<string, unknown>` | Custom key-value pairs forwarded to merchandising strategies     |

**`CartProduct`:**

| Property    | Type                     | Required | Description                             |
| ----------- | ------------------------ | -------- | --------------------------------------- |
| `title`     | `string`                 | Yes      | Product title                           |
| `price`     | `number`                 | No       | Unit price                              |
| `type`      | `string`                 | No       | Product type                            |
| `productId` | `string`                 | No       | Product ID                              |
| `variantId` | `string`                 | No       | Variant ID                              |
| `quantity`  | `number`                 | No       | Quantity in cart                        |
| `options`   | `Record<string, string>` | No       | Selected options (e.g. `{ Size: 'L' }`) |

**`CustomerContext`:**

| Property               | Type      | Description                            |
| ---------------------- | --------- | -------------------------------------- |
| `signedIn`             | `boolean` | Whether the shopper is signed in       |
| `returning`            | `boolean` | Whether the shopper has visited before |
| `numberOfOrders`       | `number`  | Lifetime order count                   |
| `averageOrderValue`    | `number`  | Average order value                    |
| `daysBetweenOrders`    | `number`  | Average days between orders            |
| `daysSinceLastOrder`   | `number`  | Days since the most recent order       |
| `daysSinceOldestOrder` | `number`  | Days since the first order             |
| `totalSpent`           | `number`  | Lifetime spend                         |

<Tip>
  Use the global `context` for values that rarely change within a session (market, channel, customer profile). Use per-query `context` for values that vary by page or interaction (current cart, geo override).
</Tip>

### Product configuration

| Option        | Type                           | Description                                                   |
| ------------- | ------------------------------ | ------------------------------------------------------------- |
| `currency`    | `string`                       | Currency for price formatting                                 |
| `formatPrice` | `(amount, currency) => string` | Custom price formatter                                        |
| `swatches`    | `Swatch[]`                     | Color swatch definitions (see below)                          |
| `includeMeta` | `boolean`                      | Include `_meta` in results (applied rules, variant breakouts) |

**Swatch:**

| Property   | Type             | Required | Description                                                      |
| ---------- | ---------------- | -------- | ---------------------------------------------------------------- |
| `name`     | `string`         | Yes      | Option name this swatch belongs to (e.g. `Color`)                |
| `value`    | `string`         | Yes      | Option value to match (e.g. `Red`)                               |
| `color`    | `string \| null` | Yes      | CSS color value (e.g. `#ff0000`)                                 |
| `imageUrl` | `string \| null` | Yes      | URL to a swatch image. Use when a color alone is not sufficient. |

### Transforms and filter aliases

Transforms post-process results before caching. Filter aliases map URL-friendly keys to API property names.

| Option                     | Type                        | Description                        |
| -------------------------- | --------------------------- | ---------------------------------- |
| `transforms.product`       | `({ base, raw }) => object` | Extend products with custom fields |
| `transforms.collection`    | `(result, raw) => result`   | Transform collection results       |
| `transforms.search`        | `(result, raw) => result`   | Transform search results           |
| `transforms.block`         | `(result, raw) => result`   | Transform block results            |
| `transforms.searchContent` | `(result, raw) => result`   | Transform content search results   |
| `transforms.filters`       | `(filters) => FilterGroup`  | Custom filter transformation       |
| `filterAliases`            | `FilterAliases`             | URL-friendly filter key mapping    |

Once configured, transforms and aliases are applied automatically:

```typescript theme={null}
import { createClient } from '@commerce-blocks/sdk'

const { data: client } = createClient({
  token: 'your-token',
  sorts: [{ name: 'Featured', code: 'featured' }],
  facets: [{ name: 'Color', code: 'options.color' }],
  attributes: ['body_html'],
  transforms: {
    product: ({ raw }) => ({
      description: raw.body_html ?? '',
      rating: raw.calculated?.average_rating ?? 0,
    }),
  },
  filterAliases: {
    color: 'options.color',
    size: 'options.size',
    brand: { property: 'vendor', values: { nike: 'Nike', adidas: 'Adidas' } },
  },
})

// Aliases resolve automatically
const collection = client.collection({ handle: 'shirts' })
await collection.execute({ filters: { color: 'Red', brand: 'nike' } })
// Products now include description and rating from the product transform
```

### Cache configuration

| Option          | Type             | Description                                                                        |
| --------------- | ---------------- | ---------------------------------------------------------------------------------- |
| `cacheLimit`    | `number`         | Max entries in cache                                                               |
| `cacheLifetime` | `number`         | TTL in milliseconds                                                                |
| `storage`       | `StorageAdapter` | Custom storage adapter for cache persistence (defaults to localStorage in browser) |
| `initialData`   | `CacheData`      | Pre-populate cache at initialization                                               |
| `restoreCache`  | `boolean`        | Auto-restore from storage on init (default: `true`)                                |

## Singleton access

After initialization, access the client anywhere:

```typescript theme={null}
import { getClient, isInitialized } from '@commerce-blocks/sdk'

if (isInitialized()) {
  const { data: client } = getClient()
  if (client) {
    // Use client
  }
}
```

## Important notes

<Info>
  All SDK methods return a `Result` type instead of throwing exceptions. Always check for `result.error` before accessing `result.data`.
</Info>

## Troubleshooting

### `401 Unauthorized` on the first request

If the first `execute()` returns an `ApiError` with status `401` (for example, `POST /api/storefront/v1/browse/frontpage 401`), the `token` passed to `createClient()` is missing, invalid, or expired.

On Shopify, the storefront token is read from the shop metafield `shop.metafields.layers.embed_settings.value.storefrontApiToken`. If that metafield is empty or absent in your theme:

1. Confirm the Layers app is installed and connected in Shopify admin.
2. Trigger a resync from **Settings → Integrations** in the Layers dashboard so the metafield is written to the shop. See [Collection & app metafields](/shopify-integration/metaobjects-metafields/app-metafields).
3. Re-render the theme and confirm the metafield now returns a non-empty value.

If the metafield is still not populated after a resync, contact Layers support.

### `"The selected sort order code is invalid."`

The `sort` you passed to `execute()` doesn't match any `sorts[].code` you registered with `createClient()`, or the registered code no longer exists in the Layers dashboard.

* Log your `sorts` config and confirm the exact `code` values match your dashboard's sort orders.
* Check the `scope` on each sort. A sort scoped to `['collection']` isn't valid on `client.search()` and vice versa.
* Omit `sort` from `execute()` to fall back to the collection's default sort order or the first configured sort.

### `"The selected facets.N is invalid."`

The `facets[N]` in your `createClient()` config references an attribute that isn't configured as a facet in Layers. `N` is the zero-based index of the offending facet.

* Remove or fix the entry at index `N` in your `facets` array.
* Attribute codes are case-sensitive and must exactly match the attribute code shown in the Layers dashboard (for example, `options.color`, not `Options.Color`).
* Facets you no longer use in the dashboard should also be removed from `createClient()`.
