Skip to main content
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.

What the shop offers

Which currencies a shop offers, and how, is the merchant’s choice in the Quickbutik admin. shop.get() states it:
Each currency has a mode, and the mode decides what the shopper sees and what they pay: 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.
shop.get() needs the checkout:read scope, which the key from the admin’s Custom storefront view carries. See Publishable keys.

Choosing a currency

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:

Reading prices

The response says what it is in. Format with the currency on the response, never with a hardcoded "SEK":
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:

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:
A missing presentment means mode: "base". In display mode, tell the shopper what they will actually pay in:

The checkout

The checkout opens in the currency the shopper browsed in, with nothing to pass. What happens there follows the mode: 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.
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.

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. 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.
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.

React

useCurrency()

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.
useClientCurrency(client) is the bare subscription (client.currency, re-rendering on a switch) when you need nothing else.

<ShopkitProvider currency>

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:
1

Read the choice on the server

lib/shopkit.ts
Clients with a cookie accessor (readOnlyShopkit(), writableShopkit() from the Next.js guide) need none of this: they read the same cookie, so the cart and the checkout follow the choice by themselves.
2

Start the provider in the same currency

app/layout.tsx
The server render and the first client paint then agree on the currency.
3

Refresh the route after a switch

components/currency-switcher.tsx
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.

Web components

<qb-currency-select>

Three ways to write it, from no markup to all of it:
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.
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.
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.

Script tag

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:

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:
See SEO.

Troubleshooting