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

useCart

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

Cart page

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.

useCheckout

Checkout button

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.
  • 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.
  • The shopper’s consent decision is appended to the hosted checkout URL, so the checkout’s own tags respect it. See Consent and analytics.
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.

Buy now

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

<Checkout>: the checkout inline

Render the hosted checkout inside your own page instead of sending the shopper away:
It renders one <div> (takes className / style) with an iframe inside, served from the checkout’s own origin.
  • 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.

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.
  • 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.
A timeout outcome is not a payment failure. Only failed is. Never tell a paying customer their payment failed because polling timed out.
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.