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

# Types

> The data shapes the kit returns: products, categories, the shop, carts, checkout sessions and confirmations.

<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. Every type here is exported from `@quickbutik/kit` and `@quickbutik/kit/sdk`.
</Info>

<Note>
  **Money is always an integer in the currency's minor unit** (öre, cents): `24900` is 249.00. The `MinorUnits` alias marks every such field. Prices include VAT. Divide by 100 or use `formatMoney`, with the currency the response states (`product.currency`, `cart.currency`). See [Currencies](/kit/concepts/currencies).
</Note>

## Page

```ts theme={null}
interface Page<T> {
  data: T[]
  has_more: boolean
  next_cursor: string | null
}
```

## Product

<ResponseField name="id" type="string">Prefixed, `"prod_123"`. Pass it straight to `cart.add`.</ResponseField>

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

<ResponseField name="price" type="number | null">Display price, minor units, in `currency`.</ResponseField>
<ResponseField name="currency" type="string | undefined">What every price on this product is in: the client's chosen currency when the shop offers it, else the shop's own. Format with this. Absent on a platform older than the field.</ResponseField>
<ResponseField name="description" type="string | null">Merchant-authored HTML.</ResponseField>

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

<ResponseField name="visible" type="boolean | null" />

<ResponseField name="taxRate" type="number | null">VAT percentage: 25, 12, 6.</ResponseField>
<ResponseField name="seoTitle" type="string | null">Preferred by `buildSeo`.</ResponseField>

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

<ResponseField name="images" type="ProductImage[]" />

<ResponseField name="options" type="ProductOptionType[] | undefined">Optional on the wire; the variant matrix rebuilds groups from the variants when absent.</ResponseField>
<ResponseField name="variants" type="ProductVariant[]">A simple product has one synthetic variant.</ResponseField>
<ResponseField name="sections" type="ProductSection[] | undefined">Merchant content sections, in order.</ResponseField>
<ResponseField name="relatedProducts" type="RelatedProducts | undefined">Only from `products.get()`.</ResponseField>

A product has **no category membership**: a category page is `products.search({ categoryId })`.

## ProductVariant

<ResponseField name="id" type="number">The numeric id the cart takes as `variantId`.</ResponseField>

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

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

<ResponseField name="weight" type="number | null">Grams.</ResponseField>

<ResponseField name="hidden" type="boolean | null" />

<ResponseField name="minPurchaseQty" type="number | null" />

<ResponseField name="maxPurchaseQty" type="number | null" />

<ResponseField name="imageId" type="number | null | undefined">The `id` of one of the product's own `images`, or `null`.</ResponseField>

<ResponseField name="price" type="ProductVariantPrice">
  <Expandable title="properties">
    <ResponseField name="price" type="number | null">Minor units. Falls back to `product.price` when null.</ResponseField>
    <ResponseField name="comparePrice" type="number | null">The "before" price; null when not on sale.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="stock" type="ProductVariantStock">
  <Expandable title="properties">
    <ResponseField name="stock" type="number | null">**`null` means not tracked, so purchasable.** Only a number at or below 0 is out of stock.</ResponseField>

    <ResponseField name="preorder" type="boolean | null" />
  </Expandable>
</ResponseField>

<ResponseField name="options" type="ProductOptionValue[]">The option values this variant is made of.</ResponseField>

## ProductImage

<ResponseField name="id" type="number" />

<ResponseField name="productId" type="number" />

<ResponseField name="url" type="string | null">Absolute, without resize parameters. **Render this.**</ResponseField>
<ResponseField name="path" type="string | null">The bare storage filename. **Not a URL**; rendering it 404s.</ResponseField>

<ResponseField name="position" type="number | null" />

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

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

<ResponseField name="contentHash" type="string | null">Cache-buster. `"temp"` means the file is still being processed.</ResponseField>

## ProductOptionType and ProductOptionValue

```ts theme={null}
interface ProductOptionType {
  id: number
  name: string | null      // "Färg", "Storlek": merchant data, differs per product
  position: number | null
}

interface ProductOptionValue {
  id: number
  name: string | null
  position: number | null
  optionId: number | null
}
```

## ProductSection

```ts theme={null}
interface ProductSection {
  id: string       // stable render key
  title: string    // may be empty
  content: string  // merchant HTML; sanitize before injecting
}
```

Rendered the way the Quickbutik theme renders them: template-linked sections carry the template's text, and the `[STOCKLEFT]`, `[PRICE]` and `[BEFOREPRICE]` merge tags are replaced.

## RelatedProducts

<ResponseField name="mode" type="'category' | 'specific' | 'none'">`category`: up to 8 from the same category, newest first. `specific`: the merchant's hand-picked list, up to 12. `none`: always empty.</ResponseField>

<ResponseField name="products" type="RelatedProductSummary[]">
  <Expandable title="properties">
    <ResponseField name="id" type="string">`"prod_123"`</ResponseField>

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

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

    <ResponseField name="price" type="{ price: number | null; comparePrice: number | null }">Priced like the product list, discounts and campaign prices included.</ResponseField>

    <ResponseField name="image" type="ProductImage | null" />
  </Expandable>
</ResponseField>

## Category

<ResponseField name="id" type="string">`"cat_12"`</ResponseField>

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

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

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

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

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

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

<ResponseField name="image" type="string | null">Bare storage filename, not a URL.</ResponseField>
<ResponseField name="imageUrl" type="string | null">Absolute. Size it with `resolveCategoryImageUrl`.</ResponseField>

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

<ResponseField name="ancestors" type="CategoryAncestor[]">
  Root first, to the immediate parent. Feeds a breadcrumb.

  <Expandable title="properties">
    <ResponseField name="id" type="string">`"cat_2"`</ResponseField>

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

    <ResponseField name="slug" type="string | null">The ancestor's own slug segment, not its full path.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="path" type="string | null">Full slug path, `"clothing/shirts"`.</ResponseField>

<ResponseField name="childCount" type="number" />

## Shop

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

<ResponseField name="currency" type="string | null | undefined">The shop's own (base) currency, upper-case ISO 4217. Absent on a platform older than the field.</ResponseField>

<ResponseField name="currencies" type="ShopCurrency[] | undefined">
  Every currency a shopper may browse in, base first. Only the base entry when the merchant's currency converter is off; read a missing field the same way.

  <Expandable title="properties">
    <ResponseField name="code" type="string">Upper-case ISO 4217.</ResponseField>
    <ResponseField name="mode" type="'base' | 'display' | 'charge'">`base`: the shop's own. `display`: shown converted, charged in the shop's currency. `charge`: priced and charged in this currency. Exported as `ShopCurrencyMode`.</ResponseField>
    <ResponseField name="rate" type="number">Units of this currency per 1 unit of the shop's currency.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="storeUrl" type="string | null | undefined" />

<ResponseField name="termsUrl" type="string | null | undefined" />

<ResponseField name="language" type="string | null | undefined">`"sv"`</ResponseField>

<ResponseField name="newsletterEnabled" type="boolean | undefined" />

<ResponseField name="messageEnabled" type="boolean | undefined" />

<ResponseField name="brand" type="ShopBrand">
  <Expandable title="properties">
    <ResponseField name="logo" type="string | null" />

    <ResponseField name="favicon" type="string | null | undefined" />

    <ResponseField name="description" type="string | null | undefined">Doubles as the home page meta description.</ResponseField>
    <ResponseField name="color.primary" type="string | null">Hex, or null when never set.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="tracking" type="ShopTracking | null | undefined">
  The merchant's tracking ids as configured in the admin, already validated by the platform. What the kit's analytics loads by default (`destinationsFromShop`). Absent on a platform older than the field.

  <Expandable title="properties">
    <ResponseField name="ga4MeasurementId" type="string | null">`G-…`</ResponseField>
    <ResponseField name="gtmContainerId" type="string | null">`GTM-…`</ResponseField>
    <ResponseField name="metaPixelId" type="string | null">Numeric pixel id.</ResponseField>
    <ResponseField name="metaCapiEnabled" type="boolean">Whether the platform sends Meta Conversions API events for this shop. Access tokens never leave the server.</ResponseField>
  </Expandable>
</ResponseField>

The wire payload also carries `demo.enabled`, not declared on the type: `true` while the shop has not activated Quickbutik Payments and its checkout runs in demo mode.

## Cart

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

<ResponseField name="storeId" type="number">The shop's **numeric** id, what the hosted checkout URL needs.</ResponseField>

<ResponseField name="items" type="CartItem[]" />

<ResponseField name="itemCount" type="number">Sum of quantities, not of lines.</ResponseField>
<ResponseField name="currency" type="string">What every amount in **this response** is in. A cart is priced per request, so the same cart can read in SEK on one request and EUR on the next.</ResponseField>

<ResponseField name="presentment" type="CartPresentment | undefined">
  How `currency` relates to the checkout. A missing field means `mode: "base"`.

  <Expandable title="properties">
    <ResponseField name="mode" type="'base' | 'display' | 'charge'">`display`: amounts converted for display, the checkout charges `chargeCurrency` (the shop's own). `charge`: amounts are what the checkout will charge.</ResponseField>
    <ResponseField name="chargeCurrency" type="string">What the checkout will charge in.</ResponseField>
    <ResponseField name="baseCurrency" type="string">The shop's own currency.</ResponseField>
    <ResponseField name="exchangeRate" type="number">Units of `currency` per 1 unit of `baseCurrency`. `1` for `base`.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="subtotal" type="number">Display figure, following the shop's VAT setting. Also `subtotalExclTax`, `subtotalInclTax`.</ResponseField>

<ResponseField name="totalTax" type="number" />

<ResponseField name="totalDiscount" type="number" />

<ResponseField name="total" type="number">Display figure. Also `totalExclTax`, `totalInclTax`.</ResponseField>

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

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

<ResponseField name="storefrontId" type="string | null | undefined">The campaign storefront the cart is bound to.</ResponseField>

<ResponseField name="surface" type="StorefrontSurface | null | undefined" />

Shipping is chosen in the hosted checkout, so a cart total covers products only.

## CartItem

<ResponseField name="id" type="string">The **line** id. The handle for `updateItem` and `removeItem`.</ResponseField>

<ResponseField name="productId" type="number" />

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

<ResponseField name="quantity" type="number" />

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

<ResponseField name="variantName" type="string | null">`"Red / XL"`; null for a simple product.</ResponseField>

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

<ResponseField name="imageUrl" type="string | null">Absolute. Size it with `applyImageTransform`.</ResponseField>
<ResponseField name="unitPrice" type="number">Display figure. Also `unitPriceExclTax`, `unitPriceInclTax`.</ResponseField>

<ResponseField name="compareAtPrice" type="number | null" />

<ResponseField name="taxRate" type="number" />

<ResponseField name="taxAmount" type="number" />

<ResponseField name="discountAmount" type="number" />

<ResponseField name="lineTotal" type="number">Display figure. Also `lineTotalTax`, `lineTotalExclTax`, `lineTotalInclTax`.</ResponseField>
<ResponseField name="available" type="boolean">False once the product was hidden or deleted.</ResponseField>

## CheckoutSession

Returned by `createSession()` and as `session` on a `v2` start result.

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

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

<ResponseField name="successUrl" type="string | null">`null` for an inline session created without one.</ResponseField>

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

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

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

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

<ResponseField name="embed" type="{ origin: string; returnUrl: string } | null | undefined" />

<ResponseField name="successMode" type="'redirect' | 'inline' | undefined" />

<ResponseField name="storefrontId" type="string | null | undefined" />

<ResponseField name="surface" type="StorefrontSurface | null | undefined" />

<ResponseField name="theme" type="'light' | 'dark'">The effective theme.</ResponseField>

<ResponseField name="displayCurrency" type="DisplayCurrency | null | undefined">
  The currency the checkout shows an approximate amount in, set when the session was created with a `"display"` currency. `null` when none applies (no currency sent, the shop's own, or a `"charge"` currency, in which case `data.pricing.value.currency` is it).

  <Expandable title="properties">
    <ResponseField name="code" type="string">Upper-case ISO 4217.</ResponseField>
    <ResponseField name="rate" type="number">Units of `code` per 1 unit of the currency the session is charged in.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="fields" type="Record<string, FieldState>">Shopper-supplied values with the server's verdict: `value`, `status` (`empty`, `valid`, `invalid`), `errors`, `updatedAt`.</ResponseField>
<ResponseField name="data" type="Record<string, DataState>">Server-computed nodes: `value`, `resolvedAt`.</ResponseField>

## CheckoutSessionSnapshot

Returned by `getSession()` and `syncCart()`. The same fields as a session (without `sessionId` and `theme`), plus:

<ResponseField name="isComplete" type="boolean">Whether the checkout can take payment.</ResponseField>
<ResponseField name="blockingFields" type="string[]">What still stands in the way.</ResponseField>

The data nodes worth reading (`CheckoutDataNodes`):

| Node | Contents |
| - | - |
| `cart_products` | The lines as the session snapshotted them: `productId`, `variantId`, `name`, `variantName`, `price`, `quantity`, `imageUrl` |
| `pricing` | Product-only subtotals, `taxOnTop`, and `currency` (what the session is priced **and charged** in), with `baseCurrency` and `exchangeRate` when that differs from the shop's own |
| `order_total` | **The authoritative total**: `subtotal`, `totalTax`, `totalDiscount`, `totalShipping`, `totalPaymentFee`, `total`, `currency`, `lines` |
| `shipping_cost`, `payment_cost`, `discount`, `promocode` | `OrderLine[]`: `id`, `tag`, `label`, `amount`, `taxRate`, `taxAmount` |

`order_total.total` is the figure the shopper is charged. Never recompute it.

## SessionConfirmation

<ResponseField name="status" type="'completed' | 'processing_payment' | 'no_attempt' | 'failed'">`processing_payment`: paid, order being created (typically 6 to 17 seconds). `no_attempt`: no payment attempt known yet.</ResponseField>

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

<ResponseField name="successMode" type="'redirect' | 'inline' | undefined" />

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

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

## ConfirmationOutcome

```ts theme={null}
type ConfirmationOutcome =
  | { kind: "completed"; orderNumber: number | null; confirmation: SessionConfirmation }
  | { kind: "failed"; confirmation: SessionConfirmation }
  | { kind: "timeout"; lastStatus: ConfirmationStatus | null }  // NOT a failure
  | { kind: "aborted" }
```

`orderNumber` can be `null` on `completed`. Fall back to `checkout.parseReturnUrl(location.href)?.orderNumber` for display.

## CurrencyInfo

What `describeCurrency(shop, selected)` and `useCurrency()` resolve a currency choice to.

<ResponseField name="currency" type="string | null">What prices are shown in: the choice when the shop offers it, else the shop's own. `null` only before the shop is known with nothing chosen.</ResponseField>
<ResponseField name="baseCurrency" type="string | null">The shop's own currency.</ResponseField>

<ResponseField name="mode" type="'base' | 'display' | 'charge'" />

<ResponseField name="rate" type="number">Units of `currency` per 1 unit of the base currency. `1` for `base`.</ResponseField>
<ResponseField name="chargeCurrency" type="string | null">What the checkout will charge: the base currency, or `currency` in charge mode.</ResponseField>
<ResponseField name="currencies" type="ShopCurrency[]">Every currency the shop offers, base first. Empty until the shop is known.</ResponseField>

## ConsentState

The shopper's cookie decision, as `shopkit.consent.read()`, `readConsentCookie()` and the consent store report it. `ConsentSnapshot` (the store's snapshot) adds `open: boolean`, whether the preferences panel is open.

<ResponseField name="status" type="'undecided' | 'decided'">Undecided is treated as denied.</ResponseField>

<ResponseField name="analytics" type="boolean" />

<ResponseField name="marketing" type="boolean" />

<ResponseField name="decidedAt" type="string | null">ISO timestamp of the decision.</ResponseField>
<ResponseField name="revision" type="number">The cookie-policy revision the decision was made under.</ResponseField>

`ConsentCategory` is `"necessary" | "analytics" | "marketing"`; `ConsentChoice` is `{ analytics: boolean; marketing: boolean }`. See [Consent and analytics](/kit/concepts/consent-and-analytics).


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