Starting a checkout
checkout.start() resolves the cart, asks the platform for a handoff, remembers the handle it gets back and returns the URL without navigating. That makes the same call work in a server action (redirect(url)), a route handler (a 302/303) and the browser (location.assign(url)).
- Vanilla
- React
- Web components
url, checkout ("v2" or "legacy"), handoffId (the handle to confirm with) and cartId. session is only present when checkout === "v2", so check before destructuring it.
Pass
cartId when the client might not remember the cart. Without it, start() checks out the cart this client remembers and creates a new, empty one when it remembers none, which the handoff refuses with 400 "Cannot hand off an empty cart to the checkout". In a browser with cookies that is fine; in a script, on a server without a cookie accessor, or inside an app builder’s preview, pass cartId: cart.id.Buy now
buyNow(item, input) is an add followed by start(): the “Köp nu” button. It uses the remembered cart, so an existing basket is carried along rather than replaced. It resolves to the same result plus cart, and checks successUrl before adding anything.
- Vanilla
- React
- Web components
successUrl: the two rules
1
It must be https
Any https host is accepted: no domain registration, no custom-domain setup.
http is accepted only for loopback (localhost, *.localhost, 127.x.x.x, ::1), so local development works against the live API. A javascript: URL or any other scheme is rejected with a 400. The SDK needs an absolute URL (${origin}/success); the elements resolve relative values for you.2
Only the origin is used
After payment the shopper lands on
<origin>/success/<orderNumber>?hash=…&t=…. The path you passed is ignored, so mount your thank-you route at /success/[orderNumber]. Static and SPA hosts need a rewrite for /success/*.successMode: "redirect". In inline mode the checkout never redirects and successUrl is optional.
backUrl: where “continue shopping” goes
The hosted checkout’s links back out (header logo, back arrow, empty-cart screen, “continue shopping” on the confirmation) all point to backUrl. Same URL rule as successUrl. It is optional: left out, the checkout falls back to the shop’s storefront URL, then to the origin of successUrl. Set it when your storefront lives on a path, or when you want the shopper back on a specific page.
successMode: redirect or inline
A redirect session without a
successUrl throws a ShopkitConfigError before any request. successMode is only sent when you set it. Because inline sessions can answer successUrl: null, CheckoutSession.successUrl is typed string | null.
Legacy-checkout shops do not support inline mode (the platform answers 400 with
details.reason: "legacy_checkout_unsupported"). Keep passing a successUrl alongside successMode: "inline" until you have verified inline mode on your shop; it is accepted and simply unused.theme: light or dark
theme is not the same as passing "light". Omit it and the checkout follows the theme the merchant chose for their shop, and keeps following it. Pass a value and this session is pinned to it. Pass it when your pages decide the look, for example a dark storefront that would otherwise flash a white checkout.
Like language and backUrl, it is set once when the session is created. On a legacy shop it is accepted and ignored. start(), createSession() and mount() take it since 1.2.0; the theme prop and attribute on <Checkout>, <qb-checkout> and <qb-checkout-button> need 1.2.1.
currency: the shopper’s currency
The checkout opens in the currency the shopper browsed in, with nothing to pass: every session and handoff carries the client’s currency. Pass currency to override it for one call, or currency: null to send none.
"charge" currency prices and charges the session in it; a "display" currency leaves the session in the shop’s currency and shows the converted amount as an approximation (session.displayCurrency). The currency is fixed when the session is created, and a second start() for the same cart in a different currency gets a new session. checkout-v2 only: a legacy shop ignores it. See Currencies.
Two checkouts, one call
Quickbutik shops are migrating between two checkouts, and which one a shop runs is the shop’s setting, not your storefront’s.start() returns the right URL either way, so location.assign(url) is the same line for both.
handoffId is the session id on v2 and the order uuid on legacy; confirmation() accepts either, so one thank-you page serves both. The post-purchase-offer token (&ppo=…) is only appended when successUrl is on a host the shop owns, so a headless storefront on its own domain should not expect it.
Older kit docs (up to 1.3) said the legacy checkout sent shoppers to the shop’s own platform storefront. The platform now honours
successUrl and backUrl on both checkouts; no code change is needed.Demo mode
A shop that has not activated Quickbutik Payments (every shop created throughPOST /v2/shops, and a claimed shop until its owner activates payments) takes the v2 branch in demo mode: the hosted checkout renders in full, the payment step shows demo methods, no money moves and no order is created. shop.get() reports demo.enabled: true on the wire. Nothing changes in your code when payments go live. See Going live.
Sessions in detail
The stale-snapshot problem
Session creation is idempotent oncartId: a second create for the same cart returns the first session, with the cart as it looked back then. That is wrong for a shopper who went to checkout, came back and added something. The kit compares the session’s cart snapshot with the live cart (product, variant, quantity; not price, which is recomputed server-side) and, when they differ, patches the session so it rehydrates from the Cart API. This happens automatically inside createSession() and start(). Call syncCart(sessionId) for a session you resume yourself.
Reading a session
CheckoutSessionSnapshot carries fields (shopper-supplied values with the server’s verdict) and data (server-computed nodes):
order_total.total is what the shopper is charged; never recompute it. isComplete says whether the checkout can take payment, and blockingFields names what stands in the way. This is also the natural way to render “what was bought” on a thank-you page, since the cart is cleared by then.
Building the hosted URL yourself
hostedUrl() builds a checkout-v2 URL without a round trip:
Prefill
status: "invalid" on that field. It never fails the create call, so a stale saved address cannot block a shopper.
The return leg
Order creation is asynchronous: the payment provider authorizes, then the platform builds the order (typically 6–17 seconds).Polling
retryAfterMs hint can raise a delay (capped at 15 s) but never lower it. A single failed poll is not terminal.
completed the kit calls finalize(): the cart id and session id are forgotten so the next visit starts a clean basket. The shop’s numeric store id is kept. orderNumber can be null on completed; fall back to the number in the URL for display.
parseReturnUrl
confirmation() says completed, never on the strength of the URL.
The thank-you page
Mount it at/success/[orderNumber] and confirm against the API:
- Vanilla
- React
- Web components
purchase analytics event once. The full Next.js version is in Next.js → Thank-you page. The session id lives in a cookie, so the storefront and the thank-you page must share an origin. Never index the page: it belongs to one shopper.
Inline confirmation
WithsuccessMode: "inline" nothing arrives on a success route. The only way to learn the order exists is the confirmation endpoint:
1
Start, remember, open
start({ successMode: "inline", backUrl }) remembers the handoff (currentSessionId()). Open url as a top-level navigation, or in a new tab if your page must stay alive.2
Poll when the shopper is plausibly back
On the page named as
backUrl, or when the tab becomes visible again, take a quick look:3
Clean up
pollConfirmation() already calls finalize() on completed. The receipt was shown by the checkout, so a small “thanks” is enough.timeout or no_attempt is the normal result for a shopper who came back without buying. Do not present it as a failure.
Analytics in the hosted checkout
The hosted checkout fires its own commerce events (begin_checkout, add_shipping_info, add_payment_info) on its own origin, with the merchant’s own GA4, GTM and Meta ids from the shop’s settings. The kit does not fire begin_checkout itself, so checkouts are not counted twice. Your thank-you page owns the purchase, and useOrderConfirmation / <qb-order-confirmation> fire it for you, deduplicated across reloads; see The purchase. Every handoff to the checkout also carries the shopper’s consent decision.
Related
Embedded checkout
Render the same checkout inside your own page.
Checkout reference
Every method, input and result type.