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

# Checkout

> Start the hosted checkout, render it inline, and confirm the order on the thank-you page.

<Info>
  This page is the curated reference. The complete, generated type reference lives at [/kit/api/core](/kit/api/core) and is regenerated from the published typings. For the flow and its rules, read [Checkout flow](/kit/concepts/checkout) and [Embedded checkout](/kit/concepts/embedded-checkout).
</Info>

The storefront never takes payment. It creates a session from a cart, hands the shopper to the hosted Quickbutik checkout (or renders that checkout in a frame), and confirms the order when the shopper comes back.

## checkout.start

```ts theme={null}
shopkit.checkout.start(input: StartCheckoutInput, options?: { signal?: AbortSignal }): Promise<StartCheckoutResult>
```

Resolves the cart, asks the platform for a handoff, remembers the handle and returns the URL. It **does not navigate**, so the same call works in a server action (`redirect(url)`), a route handler (a 302) and the browser (`location.assign(url)`). Navigate with a normal same-tab navigation; never fetch the URL and never put it in a frame of your own.

**Consent travels with the URL.** On a server client (one built with a `cookies` accessor) `start()` reads the shopper's decision from the `qb_consent` cookie and appends it to the URL as `consentCategories` (present and empty for an undecided shopper, so the checkout runs denied). When analytics is granted and the accessor has `getAll()`, it also appends `gaClientId` and `gaSessionId`, so GA4 sees one session across your site and the checkout. In a browser, the React hooks and the elements decorate the URL from the live consent store instead. `consent: false` in the config turns all of it off. See [Consent and analytics](/kit/concepts/consent-and-analytics#forwarding-consent-to-the-hosted-checkout).

### StartCheckoutInput

<ParamField body="successUrl" type="string">
  Required unless `successMode` is `"inline"`. Must be `https` (`http` only on `localhost` and loopback). **Only its origin is used**: the shopper returns to `<origin>/success/<orderNumber>?hash=…&t=…`. Must be absolute when you call the SDK.
</ParamField>

<ParamField body="cartId" type="string">
  The cart to check out. Omit it only where this client remembers the cart (a browser with cookie storage, or the client that ran `cart.add()`). Otherwise `start()` creates a new, empty cart and the handoff answers `400 "Cannot hand off an empty cart to the checkout"`.
</ParamField>

<ParamField body="backUrl" type="string">
  Where the checkout's back links point (header logo, empty cart, "continue shopping"). Same URL rule as `successUrl`. Falls back to the shop's storefront URL, then the origin of `successUrl`.
</ParamField>

<ParamField body="cancelUrl" type="string" />

<ParamField body="language" type="string">Checkout UI language, for example `"sv"`. An unknown code falls back to the shop default.</ParamField>

<ParamField body="theme" type="'light' | 'dark'">
  Paints the hosted checkout. **Omitting it is not the same as `"light"`**: omitted, the checkout follows the merchant's own setting. Fixed when the session is created.
</ParamField>

<ParamField body="successMode" type="'redirect' | 'inline'" default="redirect">
  `inline` keeps the shopper on the checkout's own confirmation, never redirects, and makes `successUrl` optional. Not supported on the legacy checkout.
</ParamField>

<ParamField body="prefill" type="SessionPrefill">
  Pre-filled customer and address fields. A value a field rejects lands as `status: "invalid"` on that field; it never fails the call.

  <Expandable title="properties">
    <ParamField body="customer" type="{ email: string; postalCode: string; customerType?: 'b2c' | 'b2b' }" />

    <ParamField body="shipping_address" type="SessionPrefillAddress">`firstName`, `lastName`, `line1`, `line2`, `city`, `postalCode`, `country`, `phone`, `company`, `companyNumber`, all optional.</ParamField>

    <ParamField body="billing_address" type="SessionPrefillAddress" />
  </Expandable>
</ParamField>

<ParamField body="storefrontId" type="string">
  A campaign storefront to attribute the checkout to. Without `cartId` it must equal the client's binding. See [Campaign storefronts](/kit/concepts/campaign-storefronts).
</ParamField>

<ParamField body="surface" type="StorefrontSurface">Where the campaign was presented. Sent only together with a storefront.</ParamField>

<ParamField body="currency" type="string | null">
  The currency the shopper is browsing in. Defaults to the client's currency; `null` sends none. The shop decides what it means: a `"charge"` currency prices and charges the session in it; a `"display"` currency leaves the session in the shop's currency and the session carries it as `displayCurrency`; anything else has no effect. Fixed when the session is created: a second `start()` for the same cart in a **different** currency gets a new session rather than the remembered one. checkout-v2 only; a legacy shop ignores it. See [Currencies](/kit/concepts/currencies#the-checkout).
</ParamField>

### StartCheckoutResult

<ResponseField name="url" type="string">The URL to navigate to. Present on every shop.</ResponseField>
<ResponseField name="checkout" type="'v2' | 'legacy'">Which checkout the shop runs. The shop decides, not the storefront.</ResponseField>
<ResponseField name="handoffId" type="string">The handle to confirm with: the session id on `v2`, the legacy order uuid on `legacy`.</ResponseField>

<ResponseField name="cartId" type="string" />

<ResponseField name="embedUrl" type="string | null">The framable URL, when the platform returned one.</ResponseField>
<ResponseField name="session" type="CheckoutSession | undefined">Only when `checkout === "v2"`. Check `checkout` before using it.</ResponseField>
<ResponseField name="legacyOrderUuid" type="string | undefined">Only when `checkout === "legacy"`.</ResponseField>

```ts theme={null}
const { url, checkout } = await shopkit.checkout.start({
  successUrl: `${location.origin}/success`,
  backUrl: `${location.origin}/`,
  language: "sv",
})
location.assign(url)
```

## checkout.buyNow

```ts theme={null}
shopkit.checkout.buyNow(item: AddCartItemInput, input: StartCheckoutInput, options?): Promise<BuyNowResult>
```

Adds one item to the remembered cart, then `start()`. An existing basket is carried along, not replaced. Checks `successUrl` before adding anything. Resolves to the start result plus `cart`, the cart as the add left it.

## checkout.createSession

```ts theme={null}
shopkit.checkout.createSession(input: CreateSessionInput, options?: { cart?: Cart; signal?: AbortSignal }): Promise<CheckoutSession>
```

The lower-level call `start()` builds on. Takes everything `StartCheckoutInput` takes (`currency` included), with `cartId` required, plus `embed: { origin, returnUrl }`. Creation is idempotent per cart server-side (and per currency: a session is only reused when its currency matches); when the existing session's cart snapshot differs from the live cart, the kit patches the session so new lines are not lost.

The session answers with the currency it ended up in:

<ResponseField name="displayCurrency" type="{ code: string; rate: number } | null">The currency the checkout shows an approximate amount in, set when the session was created with a `"display"` currency. `rate` is units of `code` per 1 unit of the currency the session is charged in. `null` when none applies.</ResponseField>
<ResponseField name="data.pricing.value.currency" type="string">The currency the session is priced **and charged** in.</ResponseField>
<ResponseField name="data.pricing.value.baseCurrency" type="string | undefined">The shop's own currency, when the session is charged in another one.</ResponseField>
<ResponseField name="data.pricing.value.exchangeRate" type="number | undefined">Units of `currency` per 1 unit of `baseCurrency`, when they differ.</ResponseField>

## checkout.getSession

```ts theme={null}
shopkit.checkout.getSession(sessionId: string, options?): Promise<CheckoutSessionSnapshot | null>
```

The session's fields and computed data. `data.order_total` is the authoritative total, products plus shipping plus fees minus discounts. `null` when the session is gone.

## checkout.syncCart

```ts theme={null}
shopkit.checkout.syncCart(sessionId: string, options?: { cart?: Cart; signal?: AbortSignal }): Promise<CheckoutSessionSnapshot | null>
```

Re-syncs a session you resume yourself with the live cart. Patches only when the snapshot actually differs. Needs `checkout:write`.

## checkout.currentSessionId

```ts theme={null}
shopkit.checkout.currentSessionId(): Promise<string | null>
```

The handoff handle `start()` remembered (24 hours). This is what a thank-you page confirms with.

## checkout.rememberedStoreId

```ts theme={null}
shopkit.checkout.rememberedStoreId(): string | null
```

The shop's numeric store id as remembered from the last checkout session or cart this client saw, or `null`. Synchronous and request-free. It is what the return leg needs after the cart is gone: the `purchase` event's dedup key on a thank-you page, or `hostedUrl()` without a cart. The id survives `finalize()`, `cart.clear()` and a cart the platform dropped, so a reloaded thank-you page builds the same key as the first visit.

## checkout.hostedUrl

```ts theme={null}
shopkit.checkout.hostedUrl(input: string | { sessionId: string; storeId?: string | number; language?: string }): string
```

Builds a checkout-v2 URL locally, `https://pay.quickbutik.com/checkout/<storeId>/<sessionId>?lang=…`, without a request. Prefer `start()`; a legacy shop's URL cannot be built locally.

`storeId` is the shop's **numeric** id, `cart.storeId`. Without it the kit uses the id remembered from the last cart, and throws a `ShopkitConfigError` when it has none. It never falls back to the key's prefix, and rejects one by name. `shopId` is accepted as a deprecated spelling of `storeId`.

## checkout.embedUrl

```ts theme={null}
shopkit.checkout.embedUrl(input: string | { sessionId: string; storeId?: string | number; language?: string }): string
```

The framable URL `mount()` uses. Build it yourself only if you drive the frame yourself; a session framed without an embed origin recorded on it is refused.

## checkout.mount

```ts theme={null}
shopkit.checkout.mount(
  container: Element | string,
  input: MountCheckoutInput,
  options?: { signal?: AbortSignal },
): Promise<EmbeddedCheckout>
```

`start()` plus a frame: one handoff that records this page's origin on the session, then an iframe in `container`. Browser only. It **never throws** for a shop or page that cannot embed; it resolves to a handle in `"fallback"` state and, unless told otherwise, sends the shopper to the full-page checkout. Mounting into a container that already shows a checkout destroys the earlier one first.

Takes everything `start()` takes, plus:

<ParamField body="minHeight" type="number" default="600">Height before the frame reports its own, and the floor.</ParamField>
<ParamField body="title" type="string" default="Checkout">The frame's accessible name.</ParamField>
<ParamField body="confirmation" type="'redirect' | 'inline'" default="redirect">`inline` keeps the receipt in the frame instead of navigating to your success route.</ParamField>
<ParamField body="handleNavigation" type="boolean" default="true">Set `false` to perform every navigation yourself, including taking redirect payment methods to the top window.</ParamField>
<ParamField body="fallbackRedirect" type="boolean" default="true">Navigate to the hosted checkout when there is nothing to frame or the frame never answers.</ParamField>
<ParamField body="handshakeTimeoutMs" type="number" default="15000">How long to wait for the frame to answer before falling back.</ParamField>
<ParamField body="embedOrigin" type="string">Defaults to this page's origin. Only for a session created by one page and framed by another.</ParamField>
<ParamField body="returnUrl" type="string">Where a redirect payment method comes back to. Defaults to this page minus the kit's own parameters. Must share an origin with `embedOrigin`.</ParamField>

## checkout.resume

```ts theme={null}
shopkit.checkout.resume(
  container: Element | string,
  sessionId: string,
  input?: { storeId?: string | number | null; language?: string; successUrl?: string } & EmbedRenderOptions,
): Promise<EmbeddedCheckout>
```

Frames an existing session: the return leg of Swish, Klarna, Vipps MobilePay, iDEAL or a full-page 3-D Secure. Pass `storeId` from `readReturnedCheckout()`; without it the id remembered with the session, then the cart's, is used.

```ts theme={null}
import { readReturnedCheckout, stripReturnedSessionId } from "@quickbutik/kit"

const returned = readReturnedCheckout()
const checkout = returned
  ? await shopkit.checkout.resume("#checkout", returned.sessionId, { storeId: returned.storeId, successUrl: "/success" })
  : await shopkit.checkout.mount("#checkout", { successUrl: "/success" })
checkout.on("ready", () => stripReturnedSessionId())
```

## EmbeddedCheckout

The handle `mount()` and `resume()` resolve to.

<ResponseField name="state" type="'embed' | 'fallback'" />

<ResponseField name="iframe" type="HTMLIFrameElement | null">`null` on a fallback handle.</ResponseField>

<ResponseField name="sessionId" type="string | null" />

<ResponseField name="on(type, handler)" type="() => void">Subscribe; returns an unsubscribe function.</ResponseField>

<ResponseField name="off(type, handler)" type="void" />

<ResponseField name="focus()" type="void">Ask the frame to focus its first control.</ResponseField>
<ResponseField name="cartUpdated()" type="void">Tell the frame the cart changed outside the kit's cart store, after the change is saved.</ResponseField>
<ResponseField name="destroy()" type="void">Remove the frame and every listener.</ResponseField>

### Events

| Event | Payload | Notes |
| - | - | - |
| `ready` | `{ sessionId, shopId, step }` | The frame is listening. |
| `height` | `{ height }` | Already applied to the frame. |
| `scroll-to` | `{ top }` | Already scrolled. |
| `navigate` | `{ url, reason }` | `reason` is `back`, `continue-shopping` or `product`. |
| `redirect` | `{ url, method, data }` | A payment provider needs the top window. `method` is `GET` or `POST`. |
| `complete` | `{ sessionId, orderNumber, successUrl }` | The order exists. |
| `demo-complete` | `{ sessionId, configureUrl }` | A demo-mode checkout finished; no order was created. |
| `error` | `{ code, message, fatal }` | `code`: `invalid-session`, `cannot-embed`, `gateway-unavailable`, `payment-failed`, `unknown`. |
| `event` | `{ name, params }` | GA4-shaped commerce events from the checkout. With the kit's analytics on, they are forwarded to the page's hub automatically. |
| `step` | `{ step }` | `contact` or `payment`. |
| `fallback` | `{ reason, url }` | `reason`: `legacy`, `no-embed-url`, `no-top-navigation`, `handshake-timeout`. |

## checkout.confirmation

```ts theme={null}
shopkit.checkout.confirmation(handoffId: string, options?): Promise<SessionConfirmation>
```

One status read. Takes a session id or a legacy order uuid.

<ResponseField name="status" type="'completed' | 'processing_payment' | 'no_attempt' | 'failed'" />

<ResponseField name="orderNumber" type="number | undefined" />

<ResponseField name="successMode" type="'redirect' | 'inline' | undefined">Treat missing as `"redirect"`.</ResponseField>

<ResponseField name="legacySuccessUrl" type="string | undefined" />

<ResponseField name="retryAfterMs" type="number | undefined">The server's polling hint.</ResponseField>

## checkout.pollConfirmation

```ts theme={null}
shopkit.checkout.pollConfirmation(handoffId: string, options?: PollConfirmationOptions): Promise<ConfirmationOutcome>
```

Polls until the order exists, the payment fails terminally, or the caps are reached. Backoff is front-loaded (1 s, 1.5 s, 1.5 s, 2 s …); the server's `retryAfterMs` can raise a delay (capped at 15 s) but never lower it. On `completed` it calls `finalize()` for you.

<ParamField body="onUpdate" type="(confirmation: SessionConfirmation) => void" />

<ParamField body="maxPolls" type="number" default="20" />

<ParamField body="maxTotalMs" type="number" default="90000" />

<ParamField body="signal" type="AbortSignal" />

`ConfirmationOutcome` is one of:

```ts theme={null}
| { kind: "completed"; orderNumber: number | null; confirmation: SessionConfirmation }
| { kind: "failed"; confirmation: SessionConfirmation }
| { kind: "timeout"; lastStatus: ConfirmationStatus | null }
| { kind: "aborted" }
```

<Warning>
  `timeout` is **not** a payment failure. The order may still land. Render "we're still processing your order", never "your payment failed". Only `failed` is terminal.
</Warning>

## checkout.finalize

```ts theme={null}
shopkit.checkout.finalize(): Promise<void>
```

Forgets the remembered cart id and session id, so the next visit starts a clean basket. The shop's numeric store id is kept (see `rememberedStoreId()`).

## checkout.parseReturnUrl

```ts theme={null}
shopkit.checkout.parseReturnUrl(url: string | URL): { orderNumber: number; hash: string | null; timestamp: number | null } | null
```

Parses the post-payment landing at `<origin>/success/<orderNumber>`. Use the number for display only: treat the order as real once `confirmation()` says `completed`, never on the strength of the URL. It never fires in `inline` success mode.

## Return-leg helpers

Exported from `@quickbutik/kit` and `@quickbutik/kit/sdk` for a page that routes the embedded checkout's redirect bounce itself. The bounce lands on the page holding the checkout with `?qb_checkout_session=<id>&qb_checkout_shop=<numeric shop id>`.

| Export | Signature | Does |
| - | - | - |
| `CHECKOUT_SESSION_PARAM` | `"qb_checkout_session"` | |
| `CHECKOUT_SHOP_PARAM` | `"qb_checkout_shop"` | |
| `readReturnedCheckout` | `(url?: string \| URL) => { sessionId: string; storeId: string \| null } \| null` | Reads both parameters and consumes nothing. Defaults to this page's URL; pass `request.url` on a server. |
| `readReturnedSessionId` | `() => string \| null` | The session id alone. |
| `stripReturnedSessionId` | `() => void` | Removes both parameters with `replaceState`. Call it from the frame's `ready` handler, not before. |
| `consumeReturnedSessionId` | `() => string \| null` | Reads and strips in one step. Not idempotent. |

## Constants

| Export | Value |
| - | - |
| `SUCCESS_MODES` | `["redirect", "inline"]` |
| `EMBED_MESSAGE_SOURCE` | `"qb-checkout-embed"` |
| `EMBED_MESSAGE_VERSION` | `1` |

`parseEmbedFrameMessage(data)` and `isSafeNavigationUrl(value)` are exported for a host that drives the frame itself with `handleNavigation: false`.


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