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

# Publishable keys

> The qb_pk_ key that drives a storefront: where to find it, what it can do, and how to keep the merchant token out of the browser.

Every kit call is authorised by a **publishable key**. It is scope-limited, tied to one shop, grants no merchant data, and is designed to be shipped to a browser. That's why a `NEXT_PUBLIC_` (or `VITE_`, or a `data-` attribute) is the right place for it.

## Format

```text theme={null}
qb_pk_<shopPrefix>_<secret>
```

The segment after `qb_pk_` is the shop's **storage prefix**, for example `101928Y`. It's the folder the shop's images live under on the CDN, and the client exposes it as `shopkit.shopPrefix`.

<Warning>
  The storage prefix is **not** the shop's numeric id. The hosted checkout URL needs the numeric id, which comes on every cart as `cart.storeId`. `checkout.start()` reads it for you. If you build a checkout URL from `shopPrefix`, the checkout answers "We couldn't find 101928Y". Since the key is tied to one shop, the kit never asks for a separate shop id option.
</Warning>

## Where to get one

<AccordionGroup>
  <Accordion title="From the Quickbutik admin (normal path)" icon="gear" defaultOpen>
    Open **Custom storefront** in the Quickbutik admin (`https://admin.quickbutik.com`) and copy the shop's publishable key. It carries every scope the kit needs: the five storefront scopes plus `storefront:read`.
  </Accordion>

  <Accordion title="No shop yet: create one through the API" icon="wand-magic-sparkles">
    One unauthenticated request creates a real, unclaimed shop and returns its keys:

    ```bash theme={null}
    curl -X POST https://api.quickbutik.com/v2/shops \
      -H "Content-Type: application/json" \
      -d '{ "name": "FriskVove", "country": "SE" }'
    ```

    The response includes a full-scope `publishable_key` for the storefront, a server-side `personal_access_token` for catalog writes, and a `claim_token`. The shop expires after 72 hours unless it's claimed by email through `POST /v2/shops/{id}/claim`. The publishable key keeps working after the claim. Rate limits are 3 per hour and 10 per day per IP. A shop created this way runs its checkout in demo mode until payments are activated. The whole flow is in [Create a shop without an account](/kit/create-a-shop).
  </Accordion>

  <Accordion title="Why the Create public key button is not enough" icon="triangle-exclamation">
    A key from the API keys page's **Create public key** button carries `storefront:read` only. The first catalog call is refused with a `403` naming `required_scopes`. Use the Custom storefront key instead.
  </Accordion>
</AccordionGroup>

## Scopes

| Scope | Grants | Used by |
| - | - | - |
| `products:read` | Products, search, variants, images **and categories** | `products.*`, `categories.*` |
| `cart:read` | Read a cart | `cart.get()`, `cart.current()` |
| `cart:write` | Create carts, add/update/remove items | `cart.create()`, `addItem()`, `updateItem()`, `removeItem()`, `delete()`, `ensure()`, `add()`, `clear()` |
| `checkout:read` | Read a session, poll confirmation, **and `shop.get()`** | `checkout.getSession()`, `confirmation()`, `pollConfirmation()`, `shop.get()` |
| `checkout:write` | Create checkout sessions and handoffs, patch a session | `checkout.createSession()`, `start()`, `buyNow()`, `syncCart()`, `mount()` |
| `storefront:read` | Pages, navigation menus, theme content, languages, translations | Raw API calls only, since the kit has no resources for these |

`DEFAULT_SHOPKIT_SCOPES` is the first five, which is the set a storefront needs to browse, build a cart and hand off. Two mappings surprise people, and both are deliberate:

* **Categories sit behind `products:read`.** They're catalog structure, not a separate surface.
* **`shop.get()` needs `checkout:read`.** It's served by the checkout's shop endpoint, the only shop surface a publishable key can reach.

A publishable key can never carry more than this set. The API refuses to issue one with a merchant scope (orders, customers, product writes).

### The local scope check

The kit checks scopes **locally, before the request leaves**, so a missing scope surfaces as a named `ShopkitScopeError` instead of an opaque `403`. The local check only knows the scopes you *declare* (the default five), not the key's real ones. The platform still enforces either way.

```ts theme={null}
createShopkitClient({ publishableKey, scopes: ["products:read", "checkout:read"] }) // declare a narrower key
createShopkitClient({ publishableKey, scopes: null })                              // disable the local check

shopkit.scopes.declared            // string[]
shopkit.scopes.has("cart:write")   // boolean
```

## Never ship a personal access token

A `qb_pat_…` token is a merchant credential. Depending on its scopes it can do anything the merchant can do. It belongs on a server or in a gitignored `.env`, never in a client bundle, a public repo or a log.

```ts theme={null}
createShopkitClient({ publishableKey: "qb_pat_…" })
// ShopkitConfigError: publishableKey must start with "qb_pk_"
```

The kit refuses a PAT outright, so it can't reach a browser through the kit. Nothing stops you from shipping it some other way, though, so keep the two apart. Both keys belong to the `/v2` API. The older `/v1` API on `api.quickbutik.com` uses its own API key and answers `401 Unauthenticated` to either of them.

## Key rotation

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

On a `401` the handler is called, the returned key is adopted, and the request is retried **once**. Concurrent 401s share a single refresh. Return `null` to let the 401 surface.

## Redacting a key in logs

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

redactKey("qb_pk_abc123_supersecret") // "qb_pk_abc123_••••"
```

The shop prefix is kept. It isn't a secret, and it's the part that makes a log line actionable.

## What a storefront key can and cannot do

<CardGroup cols={2}>
  <Card title="Can" icon="check">
    List and read visible products and variants, search and filter the catalog, read the category tree and shop branding, create and edit carts, create checkout sessions, and read session snapshots and confirmations.
  </Card>

  <Card title="Cannot" icon="ban">
    Read orders or customers, log shoppers in, apply discount codes outside the hosted checkout, take payment, or write anything on the merchant side.
  </Card>
</CardGroup>


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