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

# Quickstart

> A catalog, a cart and a hosted checkout in five minutes with @quickbutik/kit.

This page gets you from nothing to a shopper reaching the hosted checkout. Pick the tab that matches your stack. All three build the same thing: a product grid, a cart, and a checkout button.

<Steps>
  <Step title="Get a publishable key">
    In the Quickbutik admin, open **Custom storefront** and copy the shop's publishable key (`qb_pk_…`). It already carries every scope the kit needs. No shop yet? [Create one with a single request](/kit/create-a-shop). See [Publishable keys](/kit/publishable-keys) for alternatives.
  </Step>

  <Step title="Make sure products are visible">
    The storefront only sees products with `visible: true`. If the catalog looks empty, this is usually why.
  </Step>

  <Step title="Build the page">
    Pick your flavour below.
  </Step>

  <Step title="Mount the thank-you page at /success/[orderNumber]">
    After payment, the hosted checkout sends the shopper to `<origin of successUrl>/success/<orderNumber>`. Only the **origin** of `successUrl` is used, and the path is ignored.
  </Step>
</Steps>

<Tabs>
  <Tab title="Script tag">
    No npm, no bundler, no JavaScript of your own. Save this as `index.html`:

    ```html index.html theme={null}
    <!doctype html>
    <html lang="sv">
    <head>
      <meta charset="utf-8">
      <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="SEK"
        data-locale="sv-SE"></script>
      <style>
        qb-product-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 1.5rem; }
      </style>
    </head>
    <body>
      <a href="/cart">Cart (<qb-cart-count></qb-cart-count>)</a>

      <!-- The <template> must be a DIRECT child of the list element. -->
      <qb-product-list limit="12" href-template="/products/:slug">
        <template>
          <a data-qb-attr="href:product.href">
            <qb-product-image width="400" height="400"></qb-product-image>
            <h3 data-qb-text="product.name"></h3>
            <span data-qb-text="product.priceFormatted"></span>
          </a>
        </template>
      </qb-product-list>

      <qb-cart>
        <p data-qb-show="cart.empty" hidden>Your cart is empty.</p>
        <qb-cart-items>
          <template>
            <div>
              <span data-qb-text="item.productTitle"></span>
              <button type="button" data-qb-action="decrement">−</button>
              <span data-qb-text="item.quantity"></span>
              <button type="button" data-qb-action="increment">+</button>
              <strong data-qb-text="item.lineTotalFormatted"></strong>
              <button type="button" data-qb-action="remove">Remove</button>
            </div>
          </template>
        </qb-cart-items>
        <p data-qb-hide="cart.empty" hidden>Total <strong data-qb-text="cart.totalFormatted"></strong></p>
        <qb-checkout-button success-url="/success" back-url="/">
          <button type="button">Go to checkout</button>
        </qb-checkout-button>
      </qb-cart>
    </body>
    </html>
    ```

    The list above shows the catalog, but a grid card can't add to the cart on its own. Add items from a product page:

    ```html products.html theme={null}
    <qb-product slug="cotton-tee">
      <h1 data-qb-text="product.name"></h1>
      <qb-product-image width="800" height="800" priority></qb-product-image>
      <p data-qb-text="price.display"></p>
      <qb-options>
        <template>
          <fieldset>
            <legend data-qb-text="group.name"></legend>
            <qb-option-values>
              <template>
                <button type="button" data-qb-action="select" data-qb-text="value.name"></button>
              </template>
            </qb-option-values>
          </fieldset>
        </template>
      </qb-options>
      <qb-add-to-cart><button type="button">Add to cart</button></qb-add-to-cart>
    </qb-product>
    ```

    The thank-you page (served at `/success/<orderNumber>`) is one element:

    ```html success.html theme={null}
    <qb-order-confirmation>
      <p data-qb-show="confirmation.loading" hidden>Creating your order…</p>
      <p data-qb-show="confirmation.completed" hidden>Thank you! Order <b data-qb-text="confirmation.orderNumber"></b></p>
      <p data-qb-show="confirmation.failed" hidden>The payment did not go through.</p>
      <p data-qb-show="confirmation.timedOut" hidden>Your order is still being processed.</p>
    </qb-order-confirmation>
    ```

    Continue with [Web components](/kit/web-components/setup).
  </Tab>

  <Tab title="React">
    ```bash theme={null}
    npm install @quickbutik/kit
    ```

    ```tsx Shop.tsx theme={null}
    "use client"
    import {
      ShopkitProvider, useProducts, useCart, useCheckout, ProductImage,
    } from "@quickbutik/kit/react"
    import { formatMoney } from "@quickbutik/kit"

    export function Shop() {
      return (
        <ShopkitProvider config={{ publishableKey: process.env.NEXT_PUBLIC_QUICKBUTIK_PUBLISHABLE_KEY! }}>
          <Catalog />
          <CheckoutButton />
        </ShopkitProvider>
      )
    }

    function Catalog() {
      const { data, loading } = useProducts({ limit: 12 })
      const { add, pending } = useCart()
      if (loading || !data) return null
      return (
        <ul>
          {data.data.map((p) => (
            <li key={p.id}>
              <a href={`/products/${p.slug}`}>
                <ProductImage product={p} width={320} height={320} />
                {p.name} · {p.price !== null && formatMoney(p.price, "SEK")}
              </a>
              {/* A product with several variants needs a variantId; see ProductProvider */}
              {p.variants.length <= 1 && (
                <button disabled={pending > 0} onClick={() => add({ productId: p.id })}>Add</button>
              )}
            </li>
          ))}
        </ul>
      )
    }

    function CheckoutButton() {
      const { cart } = useCart()
      const { redirectToCheckout, starting } = useCheckout()
      return (
        <button
          disabled={starting || !cart?.itemCount}
          onClick={() =>
            redirectToCheckout({
              cartId: cart?.id,
              successUrl: `${location.origin}/success`,
              backUrl: location.origin,
            })
          }
        >
          Go to checkout ({cart?.itemCount ?? 0})
        </button>
      )
    }
    ```

    On the thank-you route (`/success/[orderNumber]`):

    ```tsx theme={null}
    "use client"
    import { useOrderConfirmation } from "@quickbutik/kit/react"

    export function ThankYou() {
      const { status, orderNumber, loading } = useOrderConfirmation()
      if (loading) return <p>Creating your order…</p>
      if (status === "completed") return <p>Thank you! Order #{orderNumber}</p>
      if (status === "failed") return <p>The payment did not go through.</p>
      return <p>Your order is still being processed.</p> // a timeout is NOT a failure
    }
    ```

    Continue with [React setup](/kit/react/setup), or go straight to the [Next.js App Router guide](/kit/react/nextjs).
  </Tab>

  <Tab title="Vanilla JS">
    ```bash theme={null}
    npm create vite@latest my-shop -- --template vanilla-ts
    cd my-shop && npm install @quickbutik/kit
    ```

    ```ts src/main.ts theme={null}
    import { createShopkitClient, formatMoney, pickProductImage, resolveProductImageUrl } from "@quickbutik/kit"

    const kit = createShopkitClient({
      publishableKey: import.meta.env.VITE_QUICKBUTIK_PUBLISHABLE_KEY,
    })

    const grid = document.querySelector("#grid")!
    const { data } = await kit.products.list({ limit: 12 })

    for (const product of data) {
      const card = document.createElement("article")
      const src = resolveProductImageUrl(pickProductImage(product), { width: 320, height: 320 })
      if (src) card.append(Object.assign(new Image(320, 320), { src, alt: product.name ?? "" }))
      card.append(product.name ?? "", " · ", product.price === null ? "" : formatMoney(product.price, "SEK"))

      const add = document.createElement("button")
      add.textContent = "Add to cart"
      add.onclick = () => kit.cart.add({ productId: product.id, variantId: product.variants[0]?.id })
      card.append(add)
      grid.append(card)
    }

    document.querySelector("#checkout")!.addEventListener("click", async () => {
      const { url } = await kit.checkout.start({
        successUrl: `${location.origin}/success`,
        backUrl: `${location.origin}/`,
      })
      location.assign(url) // a top-level navigation, never a fetch
    })
    ```

    <Note>
      `variants[0]` is only right for a product with a single variant. For anything with options, build a picker with the [variant matrix](/kit/vanilla/storefront#product-page).
    </Note>

    Continue with [Vanilla JS setup](/kit/vanilla/setup).
  </Tab>
</Tabs>

## Try the loop

Add a product, open the cart and click checkout. You should land in the hosted Quickbutik checkout.

<Info>
  A shop that hasn't activated **Quickbutik Payments** runs the checkout in **demo mode**. The shopper walks the real checkout, but the payment step shows demo methods, no money moves and no order is created. That's expected while you build, and nothing in your code changes when payments go live. See [Going live](/kit/concepts/going-live).
</Info>

<Note>
  A **cookie consent banner** appears on the page. That's on by default from kit 1.8.0: the kit asks the shopper, and only loads the shop's own GA4, GTM or Meta pixel (set in the Quickbutik admin) after they agree. Set `consent: false` in the config, or `data-consent="false"` on the script tag, to turn it off. See [Consent and analytics](/kit/concepts/consent-and-analytics).
</Note>

## Rules worth knowing on day one

* **Money is in minor units.** `24900` is 249,00 kr. Use `formatMoney()`, or the formatted fields the elements expose.
* **Render `image.url`, never `image.path`.** `path` is a bare filename and 404s. `<ProductImage />`, `<qb-product-image>` and `resolveProductImageUrl()` handle images for you.
* **`stock.stock === null` means "not tracked"**, so the product is purchasable. It doesn't mean sold out.
* **A confirmation `timeout` is not a payment failure.** Only `failed` is.

## Next steps

<CardGroup cols={2}>
  <Card title="Checkout flow" icon="credit-card" href="/kit/concepts/checkout">
    Sessions, the return leg and confirmation polling.
  </Card>

  <Card title="Storage & SSR" icon="cookie" href="/kit/concepts/storage-and-ssr">
    How the server and browser agree on one cart.
  </Card>
</CardGroup>


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