> For the complete documentation index, see [llms.txt](https://docs.elitebundleapp.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.elitebundleapp.com/reference/sdk.md).

# Bundle SDK

Reference for the Elite Bundle Builder SDK: live bundle state, steps, cart, discount and actions for custom Shopify bundle storefronts.

Live bundle state and actions for custom storefront UIs. One SDK covers **both** bundle types: build-your-own bundles and multi-option bundles.

Available after the `elite:byob:ready` event fires. `null` when no bundle is on the current page, and the whole `sdk` object is absent unless SDK access is enabled in **Settings → Developer SDK**.

```js
const sdk = window.eliteBundle.sdk?.byob;
```

> Read the state fresh each time you use it. The object is replaced on every update, so a reference you stashed earlier will go stale.

***

## Both bundle types, one shape

A multi-option bundle is a bundle whose every step holds exactly one item. That is why there is no separate MOB SDK — the same `steps`, `cart` and `checkout` describe both:

|              | Build your own                      | Multi-option                                            |
| ------------ | ----------------------------------- | ------------------------------------------------------- |
| `steps`      | the steps the shopper moves through | the **options** the bundle is built from                |
| `cart`       | everything the shopper picked       | one line per option                                     |
| pick an item | `addToCart` (adds / increments)     | `selectOne` (replaces that option's pick)               |
| add to cart  | `checkout()`                        | the theme's own Add to cart button, which the app hooks |

Read `steps[i].optionMode` to tell how an option is filled: `"single_product_options"` means the shopper builds a variant from one product's own options (Colour → Size); anything else means they pick from a list.

***

## State

### `bundleId`

**`string`** — Shopify product ID of the bundle.

### `steps`

**`Step[]`** — the configured steps (options, for a multi-option bundle), each with its fetched products.

```ts
interface Step {
  id: string;
  title: TranslatableString;        // see "Translated text" below
  description?: TranslatableString;
  optionMode?: "products" | "single_product_options";
  products: Product[];
  advancedSettings: {
    enableSelectionLimit: boolean;  // when false, the limits below do not apply
    minQuantity: number;
    maxQuantity: number;
    restrictToOnePerProduct: boolean;
    showVariantAsIndividualCard: boolean;
    markAsGift: boolean;
  };
}
```

The quantity limits live under `advancedSettings`, not on the step itself.

```ts
interface Product {
  id: string;
  title: string;
  handle: string;
  stepId: string;          // the step this product belongs to
  sourceProductId: string; // the real Shopify product id — see the note below
  featuredImage?: { url: string; altText?: string };
  priceRange: { minVariantPrice: { amount: string; currencyCode: string } };
  options?: Array<{ name: string; values: string[] }>;
  variants: Variant[];
}

interface Variant {
  id: string;
  title: string;
  price: string;              // decimal string, e.g. "29.99"
  compareAtPrice?: string;
  available: boolean;
  image?: { url: string; altText?: string };
  selectedOptions?: Array<{ name: string; value: string }>;
}
```

> **When `showVariantAsIndividualCard` is on**, each variant is surfaced as its own entry with a synthetic `id` and a title suffixed by the variant name. Use `sourceProductId` whenever you need the real product.

### `cart`

**`CartItem[]`** — the current selection.

```ts
interface CartItem {
  id: string;              // unique per step + variant
  stepId: string;
  productId: string;
  variantId: string;
  productHandle: string;
  title: string;
  variantTitle: string;    // "" when the product has no real variants
  imageUrl: string;        // "" when there is no image
  price: number;
  compareAtPrice: number | null;
  quantity: number;
  isGift?: boolean;
}
```

### `cartTotal` · `cartTotalFormatted` · `cartCount`

**`number`** · **`string`** · **`number`** — the sum of price × quantity, that sum run through the store's money format, and the total item count.

### `canCheckout`

**`boolean`** — every requirement is met: the bundle-wide minimum, plus the minimum of each step that has `enableSelectionLimit` on. Gate your add button on this.

### `isTierMaxReached`

**`boolean`** — the cart has reached the highest quantity tier, so there is no further discount to unlock. Only meaningful for quantity-based tiers.

### `isLoading` · `error`

**`boolean`** · **`string | null`** — `error` is a single shopper-facing message; the diagnostic detail goes to the browser console.

### `isCheckingOut` · `checkoutError`

**`boolean`** · **`string | null`** — in-flight and failure state for the add to cart.

### `discountConfig`

**`DiscountSettings`** — the bundle's discount.

```ts
interface DiscountSettings {
  discountType: "none" | "tier";
  tierBasedOn?: "quantity" | "order_total";
  discountCalculationType?: "percentage" | "fixed_price" | "fixed_amount" | "per_item";
  tiers?: Array<{ id: string; minValue: number; discountValue: number }>;
}
```

`minValue` is a **count** when `tierBasedOn` is `"quantity"` and a **money amount** when it is `"order_total"`. A multi-option bundle's flat discount is expressed as a single tier with `minValue: 0`, so it always applies.

```js
// The next tier the shopper has not reached yet
const { tiers = [], tierBasedOn = "quantity" } = sdk.discountConfig;
const current = tierBasedOn === "quantity" ? sdk.cartCount : sdk.cartTotal;
const next = [...tiers].sort((a, b) => a.minValue - b.minValue)
                       .find((t) => t.minValue > current);
```

***

## Actions

### `formatPrice(amount)`

```ts
formatPrice(amount: number): string
```

Formats an amount using the store's money format.

### `addToCart(product, variantId, stepId?)`

```ts
addToCart(product: Product, variantId: string, stepId?: string): void
```

Adds one unit. `stepId` defaults to `product.stepId`.

**It can silently do nothing** — when the bundle-wide maximum is reached, when the step's own limit is reached, or when the step allows only one of each product and that product is already in. Check `canAddToStep(stepId)` first if you need to know.

### `selectOne(product, variantId, stepId?)`

```ts
selectOne(product: Product, variantId: string, stepId?: string): void
```

**Multi-option bundles.** Makes this the step's one and only pick, replacing whatever was selected before. Use this rather than `addToCart` for options — `addToCart` would leave two items in the same option.

```js
sdk.selectOne(item.product, item.variant.id, option.id);
```

### `removeFromCart(variantId, stepId?)` · `updateQuantity(variantId, delta, stepId?)`

Remove a line, or change its quantity by `delta`. A quantity of zero or less removes the line. `updateQuantity` is subject to the same limits as `addToCart` and will no-op rather than exceed them.

### `getItemQuantity(variantId, stepId?)`

**`number`** — how many of this variant are in the cart.

### `isStepFull(stepId)` · `canAddToStep(stepId)`

**`boolean`** — whether the step has hit its own limit, and whether one more item may be added. `canAddToStep` also accounts for the bundle-wide maximum, so it can be `false` even for a step with no limit of its own.

### `checkout(redirectTarget?)`

```ts
checkout(redirectTarget?: "stay_on_page" | "cart" | "checkout"): Promise<void>
```

Adds the whole bundle to the Shopify cart. Only call it when `canCheckout` is true.

| `redirectTarget`           | Behaviour                                            |
| -------------------------- | ---------------------------------------------------- |
| `"stay_on_page"` (default) | Stays put and asks the theme to open its cart drawer |
| `"cart"`                   | Redirects to `/cart`                                 |
| `"checkout"`               | Redirects to Shopify checkout                        |

**On success the bundle is emptied** and a new bundle session begins, so a `stay_on_page` UI must be ready to re-render from an empty cart.

***

## Translated text

`title` and `description` are `TranslatableString` — either a plain string, or `{ default, translations }` when the merchant has translated them. Rendering one directly can print `[object Object]`:

```js
const text = (value, locale) =>
  typeof value === "string"
    ? value
    : value?.translations?.[locale] ?? value?.default ?? "";

text(step.title, document.documentElement.lang);
```

***

## Full example

```js
document.addEventListener("elite:byob:ready", () => {
  const sdk = window.eliteBundle.sdk?.byob;
  if (!sdk) return; // SDK access is not enabled

  sdk.steps.forEach((step) => {
    const isOption = step.optionMode !== undefined;

    step.products.forEach((product) => {
      const variant = product.variants.find((v) => v.available);
      if (!variant) return;

      const card = renderCard(product, variant, sdk.formatPrice(Number(variant.price)));
      card.addEventListener("click", () => {
        // One pick per option; many picks per step.
        if (isOption) sdk.selectOne(product, variant.id, step.id);
        else sdk.addToCart(product, variant.id, step.id);
      });
      document.querySelector(`#step-${step.id}`).appendChild(card);
    });
  });
});

document.addEventListener("elite:byob:cart-change", (event) => {
  const { cartTotal, cartCount, canCheckout } = event.detail;
  const sdk = window.eliteBundle.sdk?.byob;

  document.querySelector("#total").textContent = sdk.formatPrice(cartTotal);
  document.querySelector("#count").textContent = cartCount;
  document.querySelector("#add-btn").disabled = !canCheckout;
});
```

See the [Events Reference](/reference/events.md) for every event and its payload.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.elitebundleapp.com/reference/sdk.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
