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

# Cart and checkout

> Build the cart, hand the shopper to the hosted Quickbutik checkout (or render it inline), and confirm the order on the thank-you page.

The storefront owns the cart and the thank-you page; the hosted Quickbutik checkout owns address, shipping, discount codes and payment (Swish, Klarna, Vipps MobilePay, iDEAL, Apple Pay, Google Pay and cards). You never render a payment form. The full flow is explained in [Checkout flow](/kit/concepts/checkout).

## `useCart`

```tsx theme={null}
const {
  cart, itemCount, status, pending, error, revision,
  add, updateItem, removeItem, clear, clearCart, refresh,
} = useCart()
```

| Field | |
| - | - |
| `cart` | `Cart \| null` |
| `itemCount` | Sum of quantities, for a badge |
| `status` | `"idle" \| "loading" \| "ready" \| "error"` |
| `pending` | Number of mutations in flight. Drives an "updating…" state |
| `error` | Captured, not thrown: a failed add surfaces in the UI instead of blowing up an event handler |
| `revision` | Bumped once per successful mutation made on this page (never by a load or refresh). Compare across renders to notice that the page changed the cart |

Behaviour worth knowing:

* **One store for the whole tree.** A header badge and the cart page never disagree.
* **The cart is loaded on first mount and never created speculatively.** Mounting a badge writes no cookie and creates no cart for a visitor who only browses.
* **Mutations resolve to `null` on failure** and put the error in `error`.
* **`updateItem(itemId, 0)` removes the line.** Quantity 0 has no meaning to the API.
* **`itemId` is the cart line id** (`item.id`, a uuid), never the product id.
* **`clear()`** (alias `clearCart()`) deletes the remembered cart server-side and forgets it. With no cart it does nothing.
* **It follows the currency.** A cart stores no currency of its own; switching currency (`useCurrency().setCurrency()`) re-reads the **same** cart priced in the new one. Always format with `cart.currency`. See [Currencies](/kit/concepts/currencies).
* **Mutations are tracked.** With analytics on (the default), every successful add or remove is reported as `add_to_cart` / `remove_from_cart` once the shopper has consented.

### Header badge

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

export function CartBadge() {
  const { itemCount, pending } = useCart()
  return (
    <a href="/cart" className="badge">
      Cart <span>{pending > 0 ? "…" : itemCount}</span>
    </a>
  )
}
```

### Cart page

```tsx theme={null}
"use client"
import { applyImageTransform, formatMoney } from "@quickbutik/kit"
import { useCart, useCheckout } from "@quickbutik/kit/react"

export function CartView() {
  const { cart, status, pending, error, updateItem, removeItem, clear } = useCart()

  if (status === "loading" && !cart) return <p>Loading your cart…</p>
  if (!cart || cart.items.length === 0) return <p>Your cart is empty.</p>

  return (
    <div>
      {cart.items.map((item) => (
        <div key={item.id} className="line">
          {item.imageUrl && (
            <img src={applyImageTransform(item.imageUrl, { width: 128, height: 128, fit: "crop" })} alt="" />
          )}
          <div>
            {item.productTitle}
            {item.variantName && <small> · {item.variantName}</small>}
            {!item.available && <small> · no longer available</small>}
          </div>
          <button disabled={pending > 0} onClick={() => updateItem(item.id, item.quantity - 1)} aria-label="Decrease">−</button>
          <span>{item.quantity}</span>
          <button disabled={pending > 0} onClick={() => updateItem(item.id, item.quantity + 1)} aria-label="Increase">+</button>
          <strong>{formatMoney(item.lineTotal, cart.currency)}</strong>
          <button disabled={pending > 0} onClick={() => removeItem(item.id)}>Remove</button>
        </div>
      ))}

      <p>Subtotal {formatMoney(cart.subtotal, cart.currency)}</p>
      <p>Of which VAT {formatMoney(cart.totalTax, cart.currency)}</p>
      <p><strong>Total {formatMoney(cart.total, cart.currency)}</strong></p>
      {cart.presentment?.mode === "display" && (
        <p>Prices are shown in {cart.currency}. You pay in {cart.presentment.chargeCurrency} at checkout.</p>
      )}

      {error && <p role="alert">{error.message}</p>}
      <button onClick={() => clear()}>Empty cart</button>
      <CheckoutButton />
    </div>
  )
}
```

<Warning>
  The cart is server-owned and prices are recomputed on every read. Never cache cart totals client-side beyond the current render, and never compute VAT, discounts or shipping yourself. Shipping is chosen in the hosted checkout, so the cart shows product totals only.
</Warning>

## `useCheckout`

```tsx theme={null}
const { redirectToCheckout, start, buyNow, starting, error, url } = useCheckout()
```

| | |
| - | - |
| `redirectToCheckout(input)` | Creates the session and navigates to the hosted checkout |
| `start(input)` | Creates the session and returns `StartCheckoutResult` (or `null` on failure) **without** navigating |
| `buyNow(item, input)` | Adds one item to the remembered cart, then navigates to the checkout |
| `starting` | True while a start is in flight, and held after navigation (see below) |
| `error` | Why the last start failed |
| `url` | The last hosted checkout URL |

### Checkout button

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

export function CheckoutButton() {
  const { cart } = useCart()
  const { redirectToCheckout, starting, error } = useCheckout()

  return (
    <>
      <button
        disabled={starting || !cart?.itemCount}
        onClick={() =>
          redirectToCheckout({
            cartId: cart?.id,
            successUrl: `${location.origin}/success`,
            backUrl: `${location.origin}/`,
            language: "sv",
          })
        }
      >
        {starting ? "Opening checkout…" : "Go to checkout"}
      </button>
      {error && <p role="alert">{error.message}</p>}
    </>
  )
}
```

<Info>
  **`successUrl`: two rules.** It must be `https` (plain `http` is accepted only on `localhost` and loopback), and **only its origin is used**: the shopper returns to `<origin>/success/<orderNumber>`. Mount your thank-you route exactly there. See [Checkout flow](/kit/concepts/checkout).
</Info>

* A second click while a start is in flight is ignored.
* After `redirectToCheckout()` or `buyNow()` navigates, `starting` stays true until the page is restored from the back/forward cache, so a click while the checkout loads does nothing (for `buyNow` it would add the item twice). A navigation that never unloads the page (a "Stay" on a leave-page prompt, a 204 or download response) releases it after 10 seconds.
* Navigation uses `location.assign`, not `replace`, so the back button returns to the cart.
* `theme: "light" | "dark"` pins the hosted checkout's theme. Leaving it out lets the merchant's own setting win; it is not the same as `"light"`.
* The checkout opens in the shopper's currency (the client's), with nothing to pass. See [Currencies](/kit/concepts/currencies).
* The shopper's consent decision is appended to the hosted checkout URL, so the checkout's own tags respect it. See [Consent and analytics](/kit/concepts/consent-and-analytics).

<Tip>
  Passing `cartId: cart?.id` is optional in a normal browser, where the client remembers the cart in a cookie. Pass it anyway: inside an app builder's preview iframe cookies are blocked, and `start()` without a `cartId` would create a new, empty cart that the handoff refuses.
</Tip>

### Buy now

```tsx theme={null}
const { buyNow, starting } = useCheckout()

<button
  disabled={starting}
  onClick={() =>
    buyNow(
      { productId: product.id, variantId: variant?.id, quantity: 1 },
      { successUrl: `${location.origin}/success`, backUrl: location.href },
    )
  }
>
  Buy now
</button>
```

An existing basket is carried along, not replaced. `buyNow` goes through the shared cart store, so a badge is right if the shopper comes back with the back button.

Inside a `<ProductProvider>`, prefer `useProductAddToCart().buyNow`. It fills in the selected variant and refuses an incomplete selection:

```tsx theme={null}
function BuyButtons() {
  const { addToCart, buyNow, canAddToCart, buying, buyNowError } = useProductAddToCart()
  return (
    <>
      <button disabled={!canAddToCart} onClick={() => addToCart()}>Add to cart</button>
      <button
        disabled={!canAddToCart}
        onClick={() => buyNow({ successUrl: `${location.origin}/success`, backUrl: location.href })}
      >
        {buying ? "Opening checkout…" : "Buy now"}
      </button>
      {buyNowError && <p role="alert">{buyNowError.message}</p>}
    </>
  )
}
```

`buyNow(input, quantity = 1)` is a no-op while the selection is incomplete. `canAddToCart` is false during a buy-now too, since both buttons add.

### Starting the checkout on the server

In a server framework, starting the checkout in a server action or route handler is often better: cookies are writable there and the redirect is a real 303. See [Next.js → Checkout as a server action](/kit/react/nextjs#6-checkout-as-a-server-action).

## `<Checkout>`: the checkout inline

Render the hosted checkout inside your own page instead of sending the shopper away:

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

export default function CheckoutPage() {
  return (
    <Checkout
      successUrl="/success"
      backUrl="/cart"
      lang="sv"
      onEmpty={() => console.log("nothing to check out")}
      onComplete={({ orderNumber }) => console.log("order", orderNumber)}
      onFallback={({ reason }) => console.log("opened full-page checkout:", reason)}
    />
  )
}
```

It renders one `<div>` (takes `className` / `style`) with an iframe inside, served from the checkout's own origin.

| Prop | |
| - | - |
| `successUrl` / `backUrl` | Relative URLs resolve against the current page. Only the origin of `successUrl` is used for the landing |
| `lang` | Checkout UI language |
| `theme` | `"light"` or `"dark"`. The frame inherits nothing from your CSS, so a dark page should say so. Omitting it follows the merchant's own setting |
| `confirmation` | `"redirect"` (default) or `"inline"`, which keeps the receipt in the frame |
| `minHeight` | Height before the frame reports its own |
| `onReady` `onStep` `onEvent` `onComplete` `onError` `onFallback` | Read live, so inline arrows cost nothing |
| `onEmpty` | Called once when the cart settles empty, so no checkout is mounted |

* **The session is created once, on mount.** Changing props afterwards does not rebuild it; that would discard a session the shopper may be paying in.
* **It follows the cart.** Nothing is mounted for an empty cart. Emptying the cart from the page takes the frame down; changing the cart through `useCart()` updates the checkout in place.
* **Redirect payment methods come back on the URL.** Swish, Klarna, Vipps MobilePay, iDEAL and full-page 3-D Secure take the whole window and return with `?qb_checkout_session=` and `?qb_checkout_shop=`. The component resumes that session. Your route must render it on load and keep those parameters.
* **It falls back to the full-page checkout** on a legacy shop, when the shop is not enabled for embedding, inside a sandboxed preview, or when the frame never answers. `onFallback` receives the reason.
* Apple Pay and Google Pay are not available inline yet; they run in the full-page checkout.
* Errors are reported through `onError` and logged, never thrown during render.

Full details in [Embedded checkout](/kit/concepts/embedded-checkout).

## Thank-you page: `useOrderConfirmation`

After payment, the platform creates the order asynchronously (typically 6–17 s). The thank-you page at `/success/[orderNumber]` polls until it exists.

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

export function OrderConfirmation() {
  const { status, orderNumber, outcome, loading, error } = useOrderConfirmation()

  if (status === "failed" || outcome?.kind === "failed") {
    return <p>The payment did not go through. No order was created.</p>
  }
  if (loading) return <p>Creating your order…</p>
  if (error || outcome?.kind === "timeout") {
    // NOT a failure: the order may still land.
    return <p>Your payment is registered and your order is being created. It will show up shortly.</p>
  }
  return <p>Thank you! Order #{orderNumber} is confirmed.</p>
}
```

* With no argument it uses the checkout session the kit remembered when the checkout started. Pass a session id to override.
* **The `purchase` event is fired for you.** With analytics on (the default), a completed order is reported once, built from the checkout session's own lines and total and deduplicated across reloads. Don't fire it yourself as well.
* **The order number in the URL is never proof.** The hook confirms it against the API. Use the URL number only as a display fallback when `orderNumber` is `null`.
* On `completed` the kit forgets the cart and session ids, so the next visit starts with an empty basket.

| Option | Default | |
| - | - | - |
| `initialConfirmation` | | A snapshot the server already took with `checkout.confirmation(sessionId)`. A `completed` or `failed` one is final: nothing is polled, the result renders from the first paint, and a completed one still finalizes and fires the `purchase` |
| `enabled` | `true` | `false` when there is nothing to poll, such as a direct visit with no session |
| `trackPurchase` | `true` | `false` to report the purchase yourself |

```tsx theme={null}
// The server read a snapshot (shopkit.checkout.confirmation(sessionId)) and passed it down:
const { status, orderNumber } = useOrderConfirmation(sessionId, { initialConfirmation, enabled: sessionId !== null })
```

<Warning>
  A `timeout` outcome is **not** a payment failure. Only `failed` is. Never tell a paying customer their payment failed because polling timed out.
</Warning>

Render the thank-you page with `<SEO title="…" noIndex noFollow />`: it is one shopper's private page. The complete server-plus-client pattern is in [Next.js → Thank-you page](/kit/react/nextjs#7-thank-you-page).


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