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

# Campaign storefronts

> Sell into a campaign storefront (Säljplats) with its own prices, quantity limits and time window.

A campaign storefront, called a **Säljplats** in the Quickbutik admin, is a campaign a merchant runs beside the shop, with its own prices (percent off or fixed), its own quantity limits and its own window of time. Its id is `sf_` plus a 26-character Crockford ULID (`sf_01JBQ8ZK4M7XW9YR2TCVN3H5PD`) and is shown on the campaign in the admin.

Campaign support arrived in kit **1.3.0**; the `storefrontId` shorthand and campaign-priced product reads in **1.4.0**.

## Binding a storefront

Bind once and every product read, cart and checkout follows the campaign:

<CodeGroup>
  ```ts Vanilla theme={null}
  const shopkit = createShopkitClient({
    publishableKey,
    storefrontId: "sf_01JBQ8ZK4M7XW9YR2TCVN3H5PD",
  })
  // Long form, with a surface:
  createShopkitClient({ publishableKey, storefront: { id: "sf_…", surface: "link" } })
  ```

  ```tsx React theme={null}
  <ShopkitProvider config={config} storefrontId="sf_01JBQ8ZK4M7XW9YR2TCVN3H5PD">
    {children}
  </ShopkitProvider>
  ```

  ```html Web components theme={null}
  <!-- One section of the page -->
  <qb-shop publishable-key="qb_pk_…" storefront-id="sf_01JBQ8ZK4M7XW9YR2TCVN3H5PD">
    …
  </qb-shop>
  ```

  ```html Script tag 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-storefront-id="sf_01JBQ8ZK4M7XW9YR2TCVN3H5PD"></script>
  ```
</CodeGroup>

`storefrontId` is exactly `storefront: { id }` with the default surface. Both may be given only when they name the same campaign (case-insensitive); two different ids throw a `ShopkitConfigError`. `null` and `""` mean "not set", so `storefrontId={campaign?.id ?? null}` works. With the elements, `configure({ storefrontId })` binds the whole page.

From then on:

* **Every product read is priced for the campaign.** `products.list()`, `search()`, `get()` and everything built on them send the id, so the catalog shows what the cart will charge.
* **Every cart the client creates is bound to the campaign** and priced with its overrides on every read (`cart.storefrontId` and `cart.surface` say so).
* **Every checkout names the campaign**, so the platform applies the live-guard and quantity limits, prices the session for it and records it on the order.
* **The storage prefix defaults to `qb_<id lower-cased>`**, so the campaign cart never mixes with the shop's ordinary `qb_cart_id` cart.

<Warning>
  **Adding a storefront to an existing integration changes the storage prefix.** Carts and sessions remembered under the old `qb_…` keys are no longer read, so shoppers mid-basket start a fresh campaign cart. That is right for a campaign page and wrong for a shop that only wants attribution. To keep the old keys:

  ```ts theme={null}
  createShopkitClient({ publishableKey, storefront: { id }, storageKeyPrefix: "qb" })
  ```
</Warning>

In React, changing `storefrontId` **rebuilds the client and the cart store**, so moving from one campaign page to another does not keep showing the first campaign's basket. A prebuilt `client` prop can only confirm its own binding; one bound elsewhere throws. On `<qb-shop>`, `storefront-id` is observed the same way.

## Product reads: per-call `storefrontId`

Reads are stateless, so any campaign may be named on one, bound client or not:

```ts theme={null}
await shopkit.products.get("prod_27", { storefrontId: "sf_…" })  // priced for that campaign
await shopkit.products.search({ search: "tee", storefrontId: "sf_…" })
await shopkit.products.list({ storefrontId: null })             // a bound client, ordinary prices
```

| `storefrontId` on the read | Sent |
| - | - |
| Omitted, or `""` | The client's binding, or nothing on an unbound client |
| An id | That id |
| `null` | Nothing: the shop's ordinary prices, even on a bound client |

`useProducts` and `useProductSearch` take it as a parameter and `useProduct(id, { storefrontId })` as an option.

## Carts and checkouts: where a per-call id is allowed

Which campaign a call may name depends on whether the call owns its cart:

| Call | Per-call `storefrontId` |
| - | - |
| `cart.create({ storefrontId })` | **Any campaign.** The cart is created for it and handed back, not remembered |
| `createSession({ cartId, storefrontId })`, `start({ cartId, storefrontId })` | **Any campaign.** You named the cart |
| `cart.ensure()`, `cart.add()`, `start()` / `buyNow()` without `cartId` | **Only the client's own binding.** Anything else throws a `ShopkitConfigError` before any request |

The last row is strict because those calls reuse the remembered cart, which was created for one campaign or for none. On an unbound client a mismatch would otherwise create a campaign-priced cart and remember it as the shop's ordinary cart for thirty days. The two patterns that work:

```ts theme={null}
// 1. The whole storefront sells into one campaign: bind the client.
const shopkit = createShopkitClient({ publishableKey, storefrontId: "sf_…" })
await shopkit.cart.add({ productId })
const { url } = await shopkit.checkout.start({ successUrl })

// 2. A one-off: create a bound cart yourself and pass its id.
const cart = await shopkit.cart.create({ storefrontId: "sf_…", surface: "link" })
await shopkit.cart.addItem(cart.id, { productId })
const { url } = await shopkit.checkout.start({
  cartId: cart.id,
  successUrl,
  storefrontId: "sf_…",
  surface: "link",
})
```

`storefrontId` and `surface` travel together or not at all. A `surface` with no storefront is dropped, and with no storefront from either source the request is exactly what it was before campaigns existed.

## Surfaces

`surface` is attribution, not behaviour: it records where the campaign was presented. The vocabulary is exported as `STOREFRONT_SURFACES`:

| Surface | Meaning |
| - | - |
| `hosted` | The campaign's own page on the shop's domain |
| `link` | A short or shared link that lands on the campaign |
| `embed` | Rendered inside a third-party page |
| `shopkit` | Inside a storefront built on the kit (**the default**) |
| `agentic` | Reached by an AI agent acting for the shopper |

A malformed id or an unknown surface throws a `ShopkitConfigError` at `createShopkitClient` time. Whether the campaign exists and is open is only known at checkout.

## Handling refusals

A campaign checkout can be refused with a machine-readable reason. `storefrontRefusal(error)` decodes it and returns `null` for anything else, so it is safe on whatever was thrown:

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

try {
  const { url } = await shopkit.checkout.start({ successUrl })
  location.assign(url)
} catch (error) {
  const refusal = storefrontRefusal(error)
  if (refusal?.reason === "storefront_not_live") {
    // refusal.effectiveState: "paused" | "expired" | "coming_soon" (rarely "draft" | "archived")
    showCampaignClosed(refusal.effectiveState)
  } else if (refusal?.reason === "storefront_quantity_limit") {
    // limitType "per_order": reduce to refusal.maxQuantity
    // limitType "pool": the campaign has refusal.maxQuantity left (0 = sold out)
    showQuantityLimit(refusal.productId, refusal.variantId, refusal.maxQuantity)
  } else {
    throw error // storefront_mismatch is a programming error
  }
}
```

```ts theme={null}
type StorefrontRefusal =
  | { reason: "storefront_not_live"; storefrontId: string | null; effectiveState: string }           // 409
  | { reason: "storefront_quantity_limit"; storefrontId: string | null
      limitType: "per_order" | "pool"; productId: string | null
      variantId: number | null; maxQuantity: number | null }                                         // 409
  | { reason: "storefront_mismatch"; storefrontId: string | null; cartStorefrontId: string | null }   // 400
```

<Note>
  Campaign prices survive only on **checkout-v2**. The legacy checkout re-prices every line from the catalog, so a shop that sells on campaign pricing needs checkout-v2.
</Note>

Also exported: `isStorefrontId`, `isStorefrontSurface`, `DEFAULT_STOREFRONT_SURFACE`, `STOREFRONT_ID_PATTERN` and the `StorefrontBinding`, `StorefrontSurface` and `StorefrontAttribution` types.


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