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

# Currencies

> Let shoppers browse, and optionally pay, in another currency: the shop's offered currencies, display and charge mode, switching, server rendering and the checkout.

A shop can offer more currencies than its own. The kit asks the platform for prices in the shopper's currency, keeps the choice across visits, and opens the checkout in it. **Prices are never converted in the browser**: every amount comes from the server and states its own currency.

<CodeGroup>
  ```tsx React theme={null}
  import { useCurrency } from "@quickbutik/kit/react"

  function CurrencySwitcher() {
    const { currency, currencies, setCurrency } = useCurrency()
    if (currencies.length < 2) return null
    return (
      <select value={currency ?? ""} onChange={(e) => setCurrency(e.target.value)}>
        {currencies.map((c) => <option key={c.code} value={c.code}>{c.code}</option>)}
      </select>
    )
  }
  ```

  ```html Web components theme={null}
  <!-- Lists the shop's currencies; a choice reprices the whole page -->
  <qb-currency-select></qb-currency-select>
  ```

  ```ts Vanilla JS theme={null}
  await shopkit.setCurrency("EUR")                 // remembered for the next visit
  const { data } = await shopkit.products.list()   // priced in EUR
  ```
</CodeGroup>

## What the shop offers

Which currencies a shop offers, and how, is the merchant's choice in the Quickbutik admin. `shop.get()` states it:

```ts theme={null}
const shop = await shopkit.shop.get()
shop.currency     // "SEK", the shop's own (base) currency
shop.currencies   // [{ code: "SEK", mode: "base",    rate: 1 },
                  //  { code: "EUR", mode: "charge",  rate: 0.0871 },
                  //  { code: "NOK", mode: "display", rate: 0.95 }]
```

Each currency has a **mode**, and the mode decides what the shopper sees and what they pay:

| `mode` | Prices shown in | Checkout charges |
| - | - | - |
| `"base"` | The shop's own currency | The shop's own currency |
| `"display"` | The chosen currency, converted | The shop's own currency, with the converted amount shown as an approximation |
| `"charge"` | The chosen currency | The chosen currency |

`rate` is units of that currency per 1 unit of the shop's currency. With the merchant's currency converter switched off, `currencies` holds only the base entry, and a storefront needs no currency code at all.

<Note>
  `shop.get()` needs the `checkout:read` scope, which the key from the admin's Custom storefront view carries. See [Publishable keys](/kit/publishable-keys).
</Note>

## Choosing a currency

```ts theme={null}
const shopkit = createShopkitClient({ publishableKey, currency: "EUR" })   // the default to browse in

shopkit.currency                    // "EUR"
await shopkit.setCurrency("NOK")    // switch, and remember the choice
const off = shopkit.onCurrencyChange((code) => rerender(code))
await shopkit.setCurrency(null)     // forget the choice: back to "EUR"
```

| | |
| - | - |
| `currency` (config) | The currency to browse in until the shopper picks one. Leave it out to start in the shop's own currency. A value that is not three letters throws a `ShopkitConfigError`. |
| `shopkit.currency` | What the client browses in (`"EUR"`), or `null` for the shop's own |
| `shopkit.defaultCurrency` | The configured `currency`, ignoring any choice |
| `shopkit.setCurrency(code \| null)` | Switch and remember. `null` returns to the configured default, not necessarily to the shop's own currency |
| `shopkit.onCurrencyChange(fn)` | Subscribe to switches; returns an unsubscribe |

Once a currency is set, the kit sends it on every request that carries a price:

* **Product reads**: `list`, `search`, `get`, `getBySlug` and `listAll` carry `?currency=NOK`.
* **Cart requests**: `create`, `get`, `addItem`, `updateItem` and `removeItem` (and so `add`, `ensure` and `current`) carry it too.
* **Checkout**: every session and handoff carries `currency` in its body.

With no currency configured and none chosen, nothing is sent, and everything is in the shop's own currency.

### Per call

Any product read or checkout can name a currency for that one request, without changing the client:

```ts theme={null}
await shopkit.products.get("prod_27", { currency: "EUR" })   // this read in EUR
await shopkit.products.list({ currency: null })              // this read in the shop's own currency
await shopkit.checkout.start({ successUrl, currency: "EUR" })
```

## Reading prices

**The response says what it is in.** Format with the currency on the response, never with a hardcoded `"SEK"`:

| Response | Field |
| - | - |
| A product | `product.currency` |
| A cart | `cart.currency`, plus `cart.presentment` |
| A checkout session | `session.data.pricing.value.currency`, and `session.displayCurrency` |

```ts theme={null}
const product = await shopkit.products.get("prod_27")
formatMoney(product.price, product.currency)
```

**A currency the shop does not offer is not an error.** A well-formed code the shop does not offer (converter off, currency not enabled, no rate) silently falls back to the shop's currency, so `product.currency` can differ from `shopkit.currency`. Only a malformed code is rejected, locally, before any request.

`describeCurrency(shop, code)` resolves a choice exactly the way the platform does, which is what a switcher or a "you will be charged in SEK" note needs:

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

describeCurrency(shop, "NOK")
// { currency: "NOK", baseCurrency: "SEK", mode: "display", rate: 0.95,
//   chargeCurrency: "SEK", currencies: [...] }

describeCurrency(shop, "USD")   // not offered: resolves to the base currency, mode "base"
```

## The cart

A cart stores no currency of its own: it is **priced per request**. The same cart reads in SEK on one request and in EUR on the next, so switching currency never creates a new cart and never loses a line. The shared cart store (behind `useCart()` and the cart elements) re-reads the same cart when the currency changes, and drops a read that a later switch overtook.

`cart.presentment` says how the cart's currency relates to the checkout:

```ts theme={null}
cart.presentment
// { mode: "display", chargeCurrency: "SEK", baseCurrency: "SEK", exchangeRate: 0.95 }
```

A missing `presentment` means `mode: "base"`. In display mode, tell the shopper what they will actually pay in:

```tsx theme={null}
{cart.presentment?.mode === "display" && (
  <p>Prices are shown in {cart.currency}. You will be charged in {cart.presentment.chargeCurrency}.</p>
)}
```

## The checkout

The checkout opens in the currency the shopper browsed in, with nothing to pass. What happens there follows the mode:

| Currency | The session |
| - | - |
| A `"charge"` currency | Priced and charged in it. `session.data.pricing.value.currency` is `"EUR"`, with `baseCurrency` and `exchangeRate` beside it, and `displayCurrency` is null |
| A `"display"` currency | Stays in the shop's currency, which is what is charged. `session.displayCurrency` is `{ code: "NOK", rate }`, and the hosted checkout shows the converted amount as an approximation next to the real prices |
| Anything else | No effect; `displayCurrency` is null |

The currency is fixed when the session is created. A shopper who switches currency and goes back to the checkout gets a **new** session in the new currency rather than the remembered one.

<Note>
  Currencies apply to checkout-v2. A shop on the legacy checkout accepts the field and ignores it, so the shopper pays in the shop's own currency. See [Two checkouts, one call](/kit/concepts/checkout#two-checkouts-one-call).
</Note>

## Search filters stay in the shop's currency

`minPrice`, `maxPrice` and `sortBy: "price"` in `products.search()` run on the stored prices, so they are **always in the shop's own currency**, even when the results come back converted. Label a price filter in `shop.currency`, not in the currency the cards are shown in.

## Remembering the choice, and server rendering

The choice is stored through the client's storage adapter under `${storageKeyPrefix}_currency` (`qb_currency` by default) for a year, next to the cart id. It wins over the configured `currency` on a later visit, so the config value is a default rather than an override. The stored value is validated on every read, so a tampered cookie is ignored rather than sent.

In the browser that storage is a cookie by default, so **a server render sees the same choice**: a server client with request-cookie storage sends the same `?currency=` the browser would. See [Storage and SSR](/kit/concepts/storage-and-ssr).

Two details matter on a server:

* **`shopkit.currency` is synchronous.** Next.js's `cookies()` accessor can only answer with a promise, so there `shopkit.currency` reports the configured default. Requests still read the cookie, so prices are right; read the cookie yourself when you need the value (see the Next.js example below).
* **Clients read the stored choice live.** Until `setCurrency()` is called on it, a client reads the stored choice on every request. A choice written elsewhere (another tab, a server action, a second client with the same `storageKeyPrefix`) changes the next request without firing `onCurrencyChange`. Switch through the client that renders the page, and give clients that must browse independently their own `storageKeyPrefix`.

<Warning>
  **Anything that caches catalog data must key on the currency.** A `cache()`, an `unstable_cache`, a CDN cache key or a static page that ignores the currency will serve one shopper's euro prices to the next shopper's kronor page.
</Warning>

## React

### `useCurrency()`

```tsx theme={null}
const {
  currency, selected, baseCurrency, currencies,
  mode, rate, chargeCurrency,
  setCurrency, isLoading, error,
} = useCurrency()
```

| Field | |
| - | - |
| `currency` | What prices are shown in: the choice when the shop offers it, else the shop's own |
| `selected` | What was asked for (the remembered choice, else the configured default), offered or not |
| `baseCurrency` | The shop's own currency |
| `currencies` | `{ code, mode, rate }[]`, base first; only the base entry when the converter is off |
| `mode` | `"base"`, `"display"` or `"charge"` |
| `rate`, `chargeCurrency` | The rate used, and what the checkout will charge |
| `setCurrency(code \| null)` | Switch and remember; `null` returns to the default |
| `isLoading`, `error` | The shop read behind it: one `shop.get()` per client, shared by every caller |

**Switching is the whole integration.** `useProducts`, `useProductSearch`, `useProduct` and a `<ProductProvider slug|id>` lookup refetch in the new currency. `useCart()` re-reads the same cart in it, and the next checkout opens in it. `useProductPrice().currency` is the product's own currency, so format with that.

```tsx theme={null}
function CurrencySwitcher() {
  const { currency, currencies, mode, chargeCurrency, setCurrency, isLoading } = useCurrency()
  if (isLoading || currencies.length < 2) return null
  return (
    <label>
      Currency
      <select value={currency ?? ""} onChange={(e) => setCurrency(e.target.value)}>
        {currencies.map((c) => <option key={c.code} value={c.code}>{c.code}</option>)}
      </select>
      {mode === "display" && <small>You pay in {chargeCurrency} at checkout</small>}
    </label>
  )
}
```

`useClientCurrency(client)` is the bare subscription (`client.currency`, re-rendering on a switch) when you need nothing else.

### `<ShopkitProvider currency>`

```tsx theme={null}
<ShopkitProvider config={config} currency="EUR">
```

Shorthand for `config.currency`: the currency to browse in until the shopper picks one. Changing the prop later switches the same client with `setCurrency()`: hooks refetch and the cart is re-read, but nothing is rebuilt.

The value at mount becomes the client's default, so changing the prop to `null` afterwards returns to that mount-time value, not to the shop's own currency. Mount with no `currency` if `null` should mean the shop's own.

### Next.js App Router

A client hook can switch the browser client, but it cannot reach data a server component already fetched. The pattern from the `kit-nextjs` example:

<Steps>
  <Step title="Read the choice on the server">
    ```ts lib/shopkit.ts theme={null}
    import { cookies } from "next/headers"
    import { createShopkitClient, normalizeCurrency } from "@quickbutik/kit"

    export const CURRENCY_COOKIE = "qb_currency"   // `${storageKeyPrefix}_currency`

    // The shopper's remembered currency, or null for the shop's own.
    // Validated like the kit does, so a tampered cookie reads as "none".
    export async function requestCurrency(): Promise<string | null> {
      const value = (await cookies()).get(CURRENCY_COOKIE)?.value
      try {
        return normalizeCurrency(value)
      } catch {
        return null
      }
    }

    // A cookie-less client for catalog reads, told the currency explicitly.
    // Anything that memoizes its results must key on `currency` too.
    export function catalogShopkit(currency: string | null) {
      return createShopkitClient({ ...baseConfig(), currency, storage: "memory" })
    }
    ```

    Clients with a cookie accessor (`readOnlyShopkit()`, `writableShopkit()` from [the Next.js guide](/kit/react/nextjs)) need none of this: they read the same cookie, so the cart and the checkout follow the choice by themselves.
  </Step>

  <Step title="Start the provider in the same currency">
    ```tsx app/layout.tsx theme={null}
    const currency = await requestCurrency()

    <ShopkitProvider config={clientConfig()} initialCart={cart} currency={currency ?? undefined}>
    ```

    The server render and the first client paint then agree on the currency.
  </Step>

  <Step title="Refresh the route after a switch">
    ```tsx components/currency-switcher.tsx theme={null}
    "use client"
    import { useCurrency } from "@quickbutik/kit/react"
    import { useRouter } from "next/navigation"
    import { useTransition } from "react"

    export function CurrencySwitcher() {
      const { currency, currencies, setCurrency } = useCurrency()
      const router = useRouter()
      const [isPending, startTransition] = useTransition()
      if (currencies.length < 2) return null

      async function choose(code: string) {
        await setCurrency(code)                       // writes qb_currency; the cart re-reads
        startTransition(() => router.refresh())       // server components refetch in the new currency
      }

      return (
        <select value={currency ?? ""} disabled={isPending} onChange={(e) => void choose(e.target.value)}>
          {currencies.map((c) => <option key={c.code} value={c.code}>{c.code}</option>)}
        </select>
      )
    }
    ```

    Key anything holding server-fetched pages on the currency too (the example keys its product grid on it), so a switch drops pages priced in the old currency.
  </Step>
</Steps>

## Web components

### `<qb-currency-select>`

Three ways to write it, from no markup to all of it:

```html theme={null}
<!-- Empty: the element adds a native <select> of the shop's currencies -->
<qb-currency-select></qb-currency-select>

<!-- Your own <select>: filled for you (a value="" placeholder is kept),
     or left alone when you wrote the <option>s yourself -->
<qb-currency-select label="code-name">
  <label>Currency <select class="currency"></select></label>
</qb-currency-select>

<!-- A template: one clone per currency, any markup -->
<qb-currency-select>
  <template>
    <button type="button" data-qb-action="set-currency" data-qb-text="option.code"></button>
  </template>
</qb-currency-select>
```

A choice is remembered, and every `<qb-product>`, `<qb-product-list>` and cart element on the page reprices: the cart elements by re-reading the same cart. A switch made anywhere else (`setCurrency()`, `Quickbutik.setCurrency()`) is reflected too.

| | |
| - | - |
| Attributes | `label`: the option text, `code` (default, `EUR`), `name` (`euro`, in `locale`) or `code-name`; `locale` |
| Scope | `currency.code`, `currency.base`, `currency.mode`, `currency.chargeCurrency`, `currency.rate`, `currency.count` |
| Row scope (template) | `option.code`, `option.label`, `option.name`, `option.mode`, `option.rate`, `option.selected`, `option.base` |
| Per row | `data-currency`, `data-selected`, and `aria-pressed` on a `<button>` |
| Reflects | `state`, `data-currency`, `data-mode`, and `empty` when the shop offers only its own currency |
| Events | `qb:currency-change` with `{ currency }` |

```css theme={null}
qb-currency-select[empty] { display: none; }
```

The added `<select>` is the one piece of markup this element writes; write your own `<select>` or a `<template>` to own all of it. It reads the shop with `shop.get()`, so the key needs `checkout:read`.

### Elements follow the currency

* `<qb-product>` fetches in the client's currency and **reloads when it changes**; a product assigned as `el.product` is re-read by its id. Its `currency` attribute is only a formatting fallback for a product that does not state `product.currency`.
* `<qb-product-list>` re-runs its query on a switch, and `product.priceFormatted` uses each product's own currency.
* Cart elements format with `cart.currency`, so the right symbol follows a switch with nothing configured.

From code, `setCurrency("EUR")` (exported next to `configure()` from `@quickbutik/kit/elements`) switches the ambient shop. A `<qb-shop currency="EUR">` with its own key browses its subtree in EUR by default; changing the attribute later switches that client without rebuilding it or its cart.

<Note>
  `configure({ currency })` and `data-currency` used to be formatting settings only. They are now the currency the page **browses in** and are sent with every request. For the shop's own currency (the common setup) the platform answers exactly as before. They remain the formatting fallback for a platform older than `product.currency`.
</Note>

## Script tag

```html theme={null}
<script defer
  src="https://cdn.jsdelivr.net/npm/@quickbutik/kit@1.8.0/dist/quickbutik-kit.global.js"
  data-publishable-key="qb_pk_…"
  data-currency="EUR"></script>

<qb-currency-select></qb-currency-select>
```

`data-currency` is the currency a first visit starts in; a remembered choice wins over it. Leave it out to start in the shop's own currency. From your own scripts:

```js theme={null}
Quickbutik.currency                           // "EUR", or null for the shop's own
await Quickbutik.setCurrency("NOK")           // reprice the page; remembered. null = back to the default
const off = Quickbutik.onCurrencyChange((code) => console.log(code))
Quickbutik.describeCurrency(shop, "NOK")      // { currency, mode, chargeCurrency, … }
```

## SEO: publish the price you charge

Offers in structured data and `og:price:*` use `product.currency`. A product read in a `"display"` currency would publish a converted price that no order is ever charged. Read the product for SEO in the shop's own currency (or a `"charge"` currency), and keep the shopper's currency for the visible page:

```ts theme={null}
const product = await shopkit.products.getBySlug(slug, { currency: null })   // for buildSeo / JSON-LD
```

See [SEO](/kit/concepts/seo).

## Troubleshooting

| Symptom | Cause | Fix |
| - | - | - |
| Prices stay in SEK after `setCurrency("EUR")` | The shop does not offer EUR (converter off, currency not enabled, or no rate) | Check `shop.currencies`, or `describeCurrency(shop, "EUR").mode`. The fallback to the shop's currency is deliberate |
| `ShopkitConfigError: currency must be a three-letter ISO 4217 code` | A malformed code in `currency`, `setCurrency()` or a per-call `currency` | Pass a code like `"EUR"` |
| Prices show the wrong currency symbol | Formatting with a constant instead of the response's currency | Format with `product.currency`, `cart.currency` or `useProductPrice().currency` |
| Server-rendered prices lag one switch behind | Server components fetched before the switch | `router.refresh()` after `setCurrency()` (Next.js), or reload the route |
| One shopper sees another's currency | A cache or CDN key that ignores the currency | Key cached catalog data on the currency |
| The checkout charges SEK although the page showed NOK | NOK is a `"display"` currency: shown converted, charged in the shop's currency | Expected. Say so near the checkout button when `mode === "display"` |
| Price filters seem off by a factor | `minPrice` / `maxPrice` are in the shop's currency | Label and convert filter inputs in `shop.currency` |
| `<qb-currency-select>` renders nothing | The shop offers only its own currency (`empty` is set) | Expected; hide it with `qb-currency-select[empty]` |


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