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

# Client

> createShopkitClient, every ShopkitConfig option, and what the client exposes.

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

```ts theme={null}
import { createShopkitClient } from "@quickbutik/kit"

const shopkit = createShopkitClient({
  publishableKey: process.env.NEXT_PUBLIC_QUICKBUTIK_PUBLISHABLE_KEY!,
})
```

Available from `@quickbutik/kit` and `@quickbutik/kit/sdk`. On a script-tag page the same function is `Quickbutik.createShopkitClient`.

A client is cheap. Create **one per app in the browser** and **one per request on the server**, because the cookie accessor belongs to that request. See [Storage & SSR](/kit/concepts/storage-and-ssr).

## createShopkitClient

```ts theme={null}
function createShopkitClient(config: ShopkitConfig): ShopkitClient
```

Throws `ShopkitConfigError` synchronously for a malformed key (including a `qb_pat_…` token), a malformed campaign storefront id, an unknown `surface`, or two conflicting storefront ids.

## ShopkitConfig

Only `publishableKey` is required.

<ParamField body="publishableKey" type="string" required>
  The shop's publishable key, `qb_pk_<shopPrefix>_<secret>`. Safe to ship to a browser. A `qb_pat_…` personal access token is rejected with a `ShopkitConfigError`.
</ParamField>

<ParamField body="apiUrl" type="string" default="https://commerce.quickbutik.com">
  Commerce API origin. Leave it unset in production. A trailing `/v2` is stripped, since every request path carries its own `/v2`.
</ParamField>

<ParamField body="checkoutUrl" type="string" default="https://pay.quickbutik.com">
  Origin of the hosted checkout (checkout-v2). When you set it explicitly it **wins** over the URL the platform returns for a checkout-v2 handoff. Use it only for a preview environment or a checkout proxied onto your own domain. It is ignored for a shop on the legacy checkout.
</ParamField>

<ParamField body="imageBaseUrl" type="string">
  Fallback base for image `path`s. Rarely needed, because images already arrive with an absolute `url`. It is the shop-scoped base without the `products/` segment, for example `https://cdn.quickbutik.com/images/<shopPrefix>`.
</ParamField>

<ParamField body="scopes" type="ShopkitScope[] | string[] | null" default="DEFAULT_SHOPKIT_SCOPES">
  The scopes the key carries, used for a local pre-flight check that throws a named `ShopkitScopeError` before the request leaves the process. Pass `null` to turn the check off. The platform enforces scopes either way. See [Errors & scopes](/kit/reference/errors#scopes).
</ParamField>

<ParamField body="storage" type="'auto' | 'cookie' | 'localStorage' | 'memory' | StorageAdapter" default="auto">
  Where the cart id and checkout session id are remembered. `auto` picks server cookies when `cookies` is given, then `document.cookie`, then `localStorage`, then memory. Resolution never throws; an unusable choice degrades to memory.
</ParamField>

<ParamField body="cookies" type="CookieAccessor">
  Server-side cookie access: `get(name)`, plus optional `set(name, value, attributes)`, `remove(name, attributes)` and `getAll()`. Each may return a promise. Supplying it is what lets a server render see the browser's cart, currency and consent decision. A read-only accessor (only `get`) is valid; writes are then dropped silently.

  <Expandable title="methods">
    <ParamField body="get(name)" type="string | null | undefined" required>Read one cookie.</ParamField>
    <ParamField body="set(name, value, attributes)" type="void">Write one cookie. Omit it in a context that cannot write (a React Server Component).</ParamField>
    <ParamField body="remove(name, attributes)" type="void">Delete one cookie.</ParamField>
    <ParamField body="getAll()" type="ReadonlyArray<{ name: string; value: string }>">Every cookie on the request. Read by one thing only: `checkout.start()` uses it to find the Google Analytics `_ga_<property>` cookie (whose name is not known in advance), so the GA client and session ids reach the hosted checkout along with the consent decision. In Next.js, `() => jar.getAll()`.</ParamField>
  </Expandable>
</ParamField>

<ParamField body="cookieAttributes" type="CookieAttributes" default="{}">
  Attributes for the cookies the kit writes.

  <Expandable title="properties">
    <ParamField body="path" type="string" default="/" />

    <ParamField body="maxAge" type="number">Seconds. Defaults to the value's TTL. Fractions are floored.</ParamField>
    <ParamField body="domain" type="string">Host-only when omitted. Set a parent domain to share the cart across subdomains.</ParamField>
    <ParamField body="secure" type="boolean">True on https origins in the browser. Pass it explicitly on a server.</ParamField>
    <ParamField body="sameSite" type="'lax' | 'strict' | 'none'" default="lax">`lax` is required for the shopper's return from the hosted checkout.</ParamField>
    <ParamField body="httpOnly" type="boolean" default="false">Leave off when browser code also reads the cart.</ParamField>
  </Expandable>
</ParamField>

<ParamField body="consent" type="boolean | { revision?: number; cookieName?: string }" default="true">
  Cookie consent and the shop's analytics, **on by default**. `<ShopkitProvider>`, the elements and `checkout.start()` on a server client all read this one setting, so the browser and the server agree.

  * An object sets the cookie-policy `revision` (default `1`; bump it to ask every shopper again) and the consent `cookieName` (default `qb_consent`) for all of them at once.
  * `false` turns the whole layer off: no banner, no consent store, no analytics, and nothing appended to hosted checkout URLs.

  See [Consent and analytics](/kit/concepts/consent-and-analytics).
</ParamField>

<ParamField body="currency" type="string | null">
  The currency to browse in (`"EUR"`, any case) until the shopper picks one with `setCurrency()`. Every product read and cart request then carries `?currency=`, and every checkout session and handoff carries `currency`. Omitted or `null`: the shop's own currency, and nothing is sent. A value that is not a three-letter ISO 4217 code throws a `ShopkitConfigError`.

  It is a **default**, not an override: a choice made with `setCurrency()` is remembered under `${storageKeyPrefix}_currency` and wins on later visits. See [Currencies](/kit/concepts/currencies).
</ParamField>

<ParamField body="storefront" type="{ id: string; surface?: StorefrontSurface } | null">
  Bind this client to one campaign storefront (a Säljplats). Product reads are priced for the campaign, carts are created bound to it and checkouts are attributed to it. `surface` defaults to `"shopkit"`. Also changes the default `storageKeyPrefix`. See [Campaign storefronts](/kit/concepts/campaign-storefronts).
</ParamField>

<ParamField body="storefrontId" type="string | null">
  Shorthand for `storefront: { id }`. May be given together with `storefront` only when both name the same campaign (compared case-insensitively). `null` and `""` mean not set.
</ParamField>

<ParamField body="storageKeyPrefix" type="string" default="qb">
  Prefix for the remembered keys (`qb_cart_id`, `qb_store_id`, `qb_checkout_session`, `qb_checkout_store`, `qb_currency`). Defaults to `qb_<storefront id lower-cased>` on a client bound to a campaign. Change it to run two shops in one browser.
</ParamField>

<ParamField body="cartTtlSeconds" type="number" default="2592000">
  Lifetime of the remembered cart id (30 days).
</ParamField>

<ParamField body="fetch" type="typeof fetch" default="globalThis.fetch">
  Injected fetch, for tests or a framework's instrumented fetch.
</ParamField>

<ParamField body="timeoutMs" type="number" default="15000">
  Per-request timeout. `0` disables it.
</ParamField>

<ParamField body="maxRetries" type="number" default="2">
  Retries for transient failures (network error, 429, 5xx). Mutations carry an automatic `Idempotency-Key`, so a retried `POST /cart` cannot create a second cart. Do not wrap cart mutations in a retry loop of your own.
</ParamField>

<ParamField body="headers" type="Record<string, string>" default="{}">
  Extra headers on every request. An `authorization` header is ignored; it can never override the key.
</ParamField>

<ParamField body="onUnauthorized" type="() => string | null | Promise<string | null>">
  Called when a request answers 401. Return a fresh key to adopt it and retry the request once. Concurrent 401s share one refresh. Return `null` to let the 401 surface.
</ParamField>

## ShopkitClient

<ResponseField name="products" type="ProductsResource">
  See [Products, categories & shop](/kit/reference/products).
</ResponseField>

<ResponseField name="categories" type="CategoriesResource">
  See [Products, categories & shop](/kit/reference/products).
</ResponseField>

<ResponseField name="cart" type="CartResource">
  See [Cart](/kit/reference/cart).
</ResponseField>

<ResponseField name="checkout" type="CheckoutResource">
  See [Checkout](/kit/reference/checkout).
</ResponseField>

<ResponseField name="shop" type="ShopResource">
  See [Products, categories & shop](/kit/reference/products).
</ResponseField>

<ResponseField name="shopPrefix" type="string">
  The shop's storage prefix decoded from the key (the CDN folder its images live under). **Not** the numeric shop id the hosted checkout needs; that one is `cart.storeId`.
</ResponseField>

<ResponseField name="shopId" type="string">
  Deprecated alias of `shopPrefix`.
</ResponseField>

<ResponseField name="apiUrl" type="string">Resolved commerce API origin.</ResponseField>
<ResponseField name="checkoutUrl" type="string">Resolved hosted checkout origin.</ResponseField>
<ResponseField name="imageBaseUrl" type="string | null">Resolved image fallback base.</ResponseField>

<ResponseField name="storefront" type="{ id: string; surface: StorefrontSurface } | null">
  The campaign storefront binding, or `null`.
</ResponseField>

<ResponseField name="scopes" type="ScopeGuard">
  <Expandable title="properties">
    <ResponseField name="declared" type="readonly string[]">The scopes this client was told it has.</ResponseField>
    <ResponseField name="enforced" type="boolean">Whether the local pre-flight check is on.</ResponseField>
    <ResponseField name="has(scope)" type="boolean">Whether a scope is covered.</ResponseField>
    <ResponseField name="assert(scope, operation)" type="void">Throws `ShopkitScopeError` when not covered.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="currency" type="string | null">
  The currency the client browses in: the shopper's remembered choice, else the configured `currency`, else `null` (the shop's own). Synchronous. With an async-only cookie accessor (Next's `cookies()`) it reports the configured default; requests still read the stored choice. What prices actually came back in is on the response (`product.currency`, `cart.currency`).
</ResponseField>

<ResponseField name="defaultCurrency" type="string | null">
  The configured `currency`, ignoring any choice.
</ResponseField>

<ResponseField name="consent" type="ConsentResource">
  <Expandable title="properties">
    <ResponseField name="read()" type="Promise<ConsentState>">The shopper's decision from this client's cookies (the `cookies` accessor on a server, `document.cookie` in a browser), at this config's revision. Undecided when there is no cookie. Hand it to `<ShopkitProvider consent={{ initialState }}>` so the banner is right on first paint.</ResponseField>
    <ResponseField name="enabled" type="boolean">`false` only with `consent: false` in the config.</ResponseField>
    <ResponseField name="revision" type="number">The configured cookie-policy revision.</ResponseField>
    <ResponseField name="cookieName" type="string">The consent cookie's name, `qb_consent` by default.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="storage" type="StorageAdapter">The resolved storage adapter.</ResponseField>

<ResponseField name="runtime" type="ShopkitRuntimeInfo">
  <Expandable title="properties">
    <ResponseField name="environment" type="'browser' | 'server'" />

    <ResponseField name="storage" type="string">The adapter kind actually in use: `cookie`, `localStorage`, `memory` or your own adapter's `kind`.</ResponseField>
    <ResponseField name="persistent" type="boolean">`false` when storage degraded to memory, so the cart will not survive a reload.</ResponseField>
  </Expandable>
</ResponseField>

## setCurrency

```ts theme={null}
shopkit.setCurrency(currency: string | null): Promise<void>
```

Browse in another currency: remember it (under `${storageKeyPrefix}_currency`, for a year) and notify `onCurrencyChange` subscribers. `null` forgets the choice and returns to the configured default. The cart is unchanged: it is priced per request, so the same cart reads in the new currency next time.

Throws a `ShopkitConfigError` for a value that is not a three-letter code. A well-formed code the shop does not offer is accepted and resolves to the shop's currency server-side. See [Currencies](/kit/concepts/currencies).

## onCurrencyChange

```ts theme={null}
shopkit.onCurrencyChange(listener: (currency: string | null) => void): () => void
```

Be told when `currency` changes through `setCurrency()`. Returns an unsubscribe function. The React hooks and the elements use it to refetch prices and re-read the cart. A choice written elsewhere (another tab, a server action, a second client sharing the `storageKeyPrefix`) changes the next request without firing it.

## withStorage

```ts theme={null}
shopkit.withStorage(storage: StorageAdapter): ShopkitClient
```

Returns a copy of the client bound to different storage. The copy drops any `cookies` accessor, so the adapter you pass is the one used. This is the idiomatic way to reuse one configuration across many server requests:

```ts theme={null}
import { createShopkitClient, createRequestCookieStorage } from "@quickbutik/kit"

const base = { publishableKey: process.env.QUICKBUTIK_PUBLISHABLE_KEY! }

export function shopkitFor(request: Request, setCookies: string[]) {
  return createShopkitClient(base).withStorage(
    createRequestCookieStorage(request, { collect: setCookies, attributes: { secure: true } }),
  )
}
```

## Configuration helpers

| Export | Signature | Notes |
| - | - | - |
| `DEFAULT_API_URL` | `"https://commerce.quickbutik.com"` | |
| `DEFAULT_CHECKOUT_URL` | `"https://pay.quickbutik.com"` | |
| `resolveConfig` | `(config: ShopkitConfig) => ResolvedShopkitConfig` | Resolves a config exactly as the client does (defaults, `shopPrefix`, storefront binding, storage prefix). |
| `normalizeApiUrl` | `(url: string) => string` | Strips trailing slashes and a trailing `/v2`. |
| `PUBLISHABLE_KEY_PREFIX` | `"qb_pk_"` | |
| `parsePublishableKey` | `(key: string) => { key, shopPrefix }` | Throws `ShopkitConfigError` for anything that is not a publishable key. |
| `redactKey` | `(key: string) => string` | `"qb_pk_abc123_supersecret"` becomes `"qb_pk_abc123_••••"`. The shop prefix is kept, since it is not secret. |
| `SHOPKIT_SCOPES` | `Record<ShopkitScope, string>` | Every scope a storefront key can carry, with a description. |
| `DEFAULT_SHOPKIT_SCOPES` | `readonly ShopkitScope[]` | Every scope above except `storefront:read`. |
| `createScopeGuard` | `(scopes: readonly string[] \| null \| undefined) => ScopeGuard` | The pre-flight checker the client uses. |
| `STOREFRONT_SURFACES` | `["hosted", "link", "embed", "shopkit", "agentic"]` | |
| `DEFAULT_STOREFRONT_SURFACE` | `"shopkit"` | |
| `STOREFRONT_ID_PATTERN` | `RegExp` | `sf_` plus 26 Crockford base32 characters, case-insensitive. |
| `isStorefrontId` / `isStorefrontSurface` | `(value: unknown) => boolean` | Type guards. |
| `normalizeCurrency` | `(value: string \| null \| undefined, context?: string) => string \| null` | Upper-cases a three-letter code; `null` for empty. Throws `ShopkitConfigError` for anything else. Useful for validating a currency cookie you read yourself. |
| `describeCurrency` | `(shop, selected) => CurrencyInfo` | Resolves a currency choice against `shop.currencies` the way the platform does. See [Utilities](/kit/reference/utilities#currencies). |

## Key rotation

```ts theme={null}
createShopkitClient({
  publishableKey: currentKey,
  onUnauthorized: async () => fetchFreshKeyFromMyBackend(),
})
```

A 401 calls the handler, adopts the returned key and retries the request once. This is outside the normal retry budget: a 401 is a stale credential, not a transient failure.


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