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

# Build bundles with your own storefront UI

> Run Appstle's fixed pricing, dynamic pricing, and volume discount bundles with a fully custom storefront. Read the configuration from Shopify metafields, render your own UI with Shopify's own APIs, and let Appstle's Shopify Function apply the discount — with little or no traffic to Appstle's servers.

Many merchants love how Appstle Bundles calculates and applies discounts but want to design the customer-facing experience themselves so it matches their theme. Appstle fully supports this **headless** (or *bring-your-own-UI*) model for the three most common bundle types:

* **Fixed pricing bundle** — a set of products sold for one fixed total price.
* **Dynamic pricing bundle** — a build-your-own selection with a percentage or fixed-amount discount applied to the running total.
* **Volume discount bundle** — tiered "buy more, save more" discounts based on quantity or spend.

<Note>
  **The mental model in one sentence:** you use the *exact same* Shopify metafields, cart-line attributes, and discount Functions that Appstle's own bundle widgets use — the only thing you replace is the UI. Nothing about how the bundle is configured, priced, or discounted changes; you are simply rendering the experience yourself instead of letting Appstle's widget render it.
</Note>

This page explains the architecture that makes headless integrations possible. The per-bundle guides then walk through each type end to end.

<CardGroup cols={3}>
  <Card title="Fixed pricing bundle" icon="box" href="/bundles/fixed-pricing-bundle">
    Sell a curated set for one fixed price.
  </Card>

  <Card title="Dynamic pricing bundle" icon="boxes-stacked" href="/bundles/dynamic-pricing-bundle">
    Build-your-own with a percentage or amount discount.
  </Card>

  <Card title="Volume discount bundle" icon="layer-group" href="/bundles/volume-discount-bundle">
    Tiered "buy more, save more" discounts.
  </Card>
</CardGroup>

## Design principle: lean on Shopify, not on Appstle

<Info>
  **Read configuration from Shopify metafields, fetch product data from Shopify's own APIs, compute previews on the client, and let the discount apply through a Shopify Function.** Treat calls to Appstle's servers as a last resort, and cache them when you do make them. This is exactly how Appstle's own storefront widgets are built — they read everything from a Shopify metafield and almost never call back to Appstle at render time. Following the same pattern keeps your storefront fast and resilient, and avoids putting storefront-scale traffic on Appstle's API.
</Info>

The key idea: **you never calculate or apply the final discount yourself**. The merchant configures the bundle once in the Appstle admin, and Appstle provisions a **Shopify automatic discount backed by a Shopify Function** in the store, and publishes the rules to a **shop metafield**. Your storefront reads the rules from that metafield, renders a selector, and adds the customer's chosen line items to the cart **with a small set of line-item attributes**. The Appstle discount Function detects those attributes and applies the savings automatically — in the cart, in checkout, and on the final order.

```mermaid theme={null}
flowchart LR
  A[Merchant configures bundle<br/>in Appstle admin] --> B[Appstle publishes rules to a<br/>Shopify metafield + provisions<br/>an automatic discount Function]
  B --> C[Your storefront reads the rules<br/>from the Shopify metafield]
  C --> D[You render your own selector<br/>using Shopify product data]
  D --> E[Add line items to cart<br/>with _appstle-* attributes]
  E --> F[Appstle Function detects the<br/>attributes and applies the discount]
```

This design has three important consequences:

<CardGroup cols={3}>
  <Card title="No discount codes" icon="ban">
    Discounts are **automatic**, not code-based. You do not request, generate, or apply a discount code anywhere in the flow.
  </Card>

  <Card title="The Function is authoritative" icon="shield-check">
    The Shopify Function decides the final discount at checkout. Any number you show in your UI is a **preview** — compute it from the same rule fields.
  </Card>

  <Card title="Attributes are mandatory" icon="tags">
    If the required `_appstle-*` line attributes are missing, the Function cannot recognize the line and **no discount is applied**.
  </Card>
</CardGroup>

<Warning>
  The legacy `PUT /bundles/bb/api/build-a-box/discount/{token}` endpoint is **deprecated** and no longer returns a usable discount — discounting moved entirely to automatic Shopify Functions. Do not build a headless flow around it. Apply the line attributes described below instead.
</Warning>

## Prerequisites

<Steps>
  <Step title="Install Appstle Bundles and keep it active">
    The discount Functions and the rules metafield only exist while the app is installed. Uninstalling removes the automatic discounts.
  </Step>

  <Step title="Create and activate the bundle in the Appstle admin">
    Configure the bundle (products, pricing, tiers) in the Appstle dashboard and set its status to **Active**. This provisions the Shopify automatic discount and publishes the rule to the shop metafield. Bundles are authored in the admin; there is no API to create them.
  </Step>

  <Step title="Add the Appstle app block once (themes)">
    On a Shopify theme, enable the Appstle Bundles app embed/block so the rules metafield is available to your code. You can hide Appstle's default widget and render your own UI from the same data.
  </Step>
</Steps>

## The integration model

Every headless bundle, regardless of type, follows the same shape — and most of it runs against Shopify, not Appstle:

<Steps>
  <Step title="Read the rules from the Shopify metafield">
    Appstle publishes active rules to the **`appstle_bundles.bundle_rules`** shop metafield. Read it the Shopify-native way — see [Reading bundle configuration](#reading-bundle-configuration). No Appstle server call.
  </Step>

  <Step title="Render your own selector with Shopify data">
    Build UI from the rule fields: list eligible products, enforce the selection limits, and show a running total. Use **Shopify's own** product/variant data (Liquid objects or the Storefront API) for titles, images, and prices.
  </Step>

  <Step title="Validate the selection, then add to cart">
    When the selection is valid, add the line items via Shopify's **Ajax Cart API** (themes) or the **Storefront API `cartLinesAdd`** mutation (fully headless), attaching the [bundle line attributes](#required-cart-line-attributes).
  </Step>

  <Step title="Let the discount apply automatically">
    Appstle's discount Function reads the attributes and applies the savings in cart and checkout. You do nothing else. Optionally render a [discount preview](#previewing-the-discount) computed on the client from the same rule fields.
  </Step>
</Steps>

## Reading bundle configuration

Pick the source closest to Shopify. The first option should cover almost every storefront.

<Tabs>
  <Tab title="Shop metafield (recommended)">
    Appstle publishes all active bundle rules to the shop metafield **`appstle_bundles.bundle_rules`**. This is the same data that drives Appstle's own widgets (exposed there as `window._ABConfig.bundle_rules`), and it contains every rule type — fixed pricing, dynamic pricing, and volume discount.

    On a Shopify theme, read it directly in Liquid — **no network call to anyone**:

    ```liquid theme={null}
    {% assign bundle_rules = shop.metafields.appstle_bundles.bundle_rules.value %}
    <script>
      window.MY_BUNDLE_RULES = {{ bundle_rules | json }};
    </script>
    ```

    Then filter to the bundle you want by `bundleType` and `uniqueRef`:

    ```js theme={null}
    const rules = window.MY_BUNDLE_RULES || [];
    const volumeBundles = rules.filter(r => r.bundleType === 'VOLUME_DISCOUNT' && r.status === 'ACTIVE');
    ```

    <Note>
      Several rule fields are themselves **JSON-encoded strings** inside the metafield (for example `tieredDiscount`, `products`, `variants`). Parse them with `JSON.parse(...)` before use — exactly as Appstle's own widget does.
    </Note>
  </Tab>

  <Tab title="Shopify Storefront API (fully headless)">
    On a non-theme stack (Hydrogen, a custom frontend), fetch the **same shop metafield** through Shopify's Storefront API rather than calling Appstle:

    ```graphql theme={null}
    query BundleRules {
      shop {
        metafield(namespace: "appstle_bundles", key: "bundle_rules") {
          value
        }
      }
    }
    ```

    `value` is a JSON string — parse it into the rules array. Hydrate product titles, images, and prices with Shopify's Storefront API as usual. If the metafield is not exposed to the Storefront API for your store, read it server-side via the Shopify Admin API and cache it.
  </Tab>

  <Tab title="Appstle App Proxy (last resort)">
    Appstle exposes build-a-box configuration through your store's **Shopify App Proxy**. Use these only when you need server-parsed product hydration that you cannot get from Shopify directly, and **cache the responses** — they are not meant for per-request, storefront-scale traffic.

    | Endpoint (via App Proxy)                              | Returns                                                                                                                                                                             |
    | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `GET /bundles/bb/api/build-a-box/by-token/{token}`    | A single bundle rule, where `{token}` is the rule's `uniqueRef`.                                                                                                                    |
    | `GET /bundles/bb/api/build-a-box/get-bundle/{handle}` | The rule plus a **parsed `products` array** (`productId`, `variantId`, `name`, `productHandle`, `imageSrc`, `price`, `minQuantity`, `maxQuantity`, `mandatory`, …) ready to render. |
    | `GET /bundles/bb/api/build-a-box/single-product`      | All active fixed-pricing build-a-box rules for the shop.                                                                                                                            |
  </Tab>

  <Tab title="Appstle Admin API (backend only)">
    For **server-side** use cases — ERP sync, reporting, internal tooling — read rules with the `X-API-Key` Admin API. This requires a secret key and **must never run in a browser or storefront context**. See [Authentication](/bundles/authentication).

    ```bash theme={null}
    curl -X GET \
      "https://bundles-admin.appstle.com/api/external/bundle-rules?shop=${SHOP}&status.equals=ACTIVE" \
      -H "X-API-Key: ${APPSTLE_API_KEY}"
    ```
  </Tab>
</Tabs>

## Bundle type reference

Each Appstle bundle type maps to a `bundleType` value in the rules data. These three are supported for headless storefronts:

| Merchant label         | `bundleType`                 | `bundleSubType` |
| ---------------------- | ---------------------------- | --------------- |
| Fixed pricing bundle   | `SINGLE_PRODUCT_BUILD_A_BOX` | `FIXED_BUNDLE`  |
| Dynamic pricing bundle | `CLASSIC_BUILD_A_BOX`        | —               |
| Volume discount bundle | `VOLUME_DISCOUNT`            | —               |

<Note>
  Other values exist in the `bundleType` enum (for example `DISCOUNTED_PRICING`, `BUY_X_GET_Y`, `SECTIONED_BUNDLE`, `COMBO_BUNDLE`). This documentation covers the three types above; the same metafield-plus-attributes model extends to the others.
</Note>

### Discount fields on a rule

The relevant pricing fields on each rule object:

| Field                                 | Type          | Used by         | Meaning                                                                                                    |
| ------------------------------------- | ------------- | --------------- | ---------------------------------------------------------------------------------------------------------- |
| `uniqueRef`                           | string        | all             | Stable reference for the bundle. **You attach this to every cart line** as `_appstle-bb-id`.               |
| `discountType`                        | enum          | dynamic, volume | One of `PERCENTAGE`, `FIXED_AMOUNT`, `FIXED_BUNDLE_AMOUNT`, `TIERED_DISCOUNT`, `NO_DISCOUNT`, `FREE_GIFT`. |
| `discountValue`                       | number        | dynamic         | The percentage (e.g. `15` = 15%) or fixed amount, depending on `discountType`.                             |
| `price`                               | number        | fixed           | The fixed total price the customer pays for the bundle.                                                    |
| `tieredDiscount`                      | string (JSON) | volume          | JSON array of tier objects (see the [Volume discount guide](/bundles/volume-discount-bundle)).             |
| `minProductCount` / `maxProductCount` | integer       | dynamic, fixed  | Min / max number of items the customer must select.                                                        |
| `minOrderAmount` / `maxOrderAmount`   | number        | dynamic         | Min / max selection value required for the discount to apply.                                              |
| `products` / `variants`               | string (JSON) | all             | Eligible products and variants configured for the bundle.                                                  |
| `status`                              | string        | all             | The bundle is only live when `ACTIVE`.                                                                     |

## Required cart line attributes

This is the contract between your storefront and Appstle's discount Function. When you add a bundle line item to the cart, attach these attributes. On a Shopify theme they are passed as line-item **`properties`**; through the Storefront API they are line **`attributes`**. Both surface to the Function as cart-line attributes.

| Attribute key              | Required                       | Value                                           | Purpose                                                                        |
| -------------------------- | ------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------ |
| `_appstle-bb-id`           | **Yes**                        | The rule's `uniqueRef`                          | Links the line to a specific bundle so the Function knows which rule to apply. |
| `_appstle_bundles_type`    | **Yes**                        | The `bundleType` value (e.g. `VOLUME_DISCOUNT`) | Tells the Function which discount logic to run.                                |
| `__appstle-bb-name`        | Recommended                    | The bundle's `name`                             | Human-readable label shown in cart/order line items.                           |
| `_appstle_bundle_combo_id` | Volume only (tier restriction) | The selected tier's identifier                  | Pins the line to a specific tier when *tier restriction* is enabled.           |
| `_appstle-bb-child`        | Fixed only (child items)       | `"true"`                                        | Marks component lines added alongside the parent in certain inventory modes.   |

<Tip>
  Attribute **keys are matched exactly**, including the leading underscore and the mix of hyphens and underscores (`_appstle-bb-id` vs. `_appstle_bundles_type`). Copy them verbatim. Keys beginning with `_` are hidden from the customer in standard Shopify cart/checkout UIs.
</Tip>

### Adding to cart on a Shopify theme (Ajax Cart API)

```js theme={null}
await fetch(`${Shopify.routes.root}cart/add.js`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [
      {
        id: variantId,          // numeric Shopify variant ID
        quantity: 2,
        properties: {
          '_appstle-bb-id': bundle.uniqueRef,
          '_appstle_bundles_type': bundle.bundleType,
          '__appstle-bb-name': bundle.name,
        },
      },
    ],
  }),
});
```

### Adding to cart on a fully headless stack (Storefront API)

```graphql theme={null}
mutation AddBundleLines($cartId: ID!) {
  cartLinesAdd(
    cartId: $cartId
    lines: [
      {
        merchandiseId: "gid://shopify/ProductVariant/123456789"
        quantity: 2
        attributes: [
          { key: "_appstle-bb-id", value: "your-bundle-uniqueRef" }
          { key: "_appstle_bundles_type", value: "VOLUME_DISCOUNT" }
          { key: "__appstle-bb-name", value: "Summer Skincare Set" }
        ]
      }
    ]
  ) {
    cart { id }
    userErrors { field message }
  }
}
```

## Previewing the discount

Because the Shopify Function applies the authoritative discount at checkout, any amount you display beforehand is a **preview** — and you compute it **on the client**, from the same rule fields, with no server call. (Appstle's own widget does the same, and even exposes a `window._ABConfig.getProductDiscountedPricing(...)` helper on theme storefronts.) The formula differs per type — see each guide:

* [Fixed pricing](/bundles/fixed-pricing-bundle#previewing-savings) — savings = sum of line prices − `price`.
* [Dynamic pricing](/bundles/dynamic-pricing-bundle#previewing-the-discount) — percentage or fixed amount off the eligible subtotal.
* [Volume discount](/bundles/volume-discount-bundle#previewing-the-discount) — the best-matching tier's discount.

<Note>
  All monetary values in the Shopify cart are expressed in the **minor unit** (cents). A `total_price` of `5000` means `$50.00`. Account for this when computing previews.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Fixed pricing bundle guide" icon="box" href="/bundles/fixed-pricing-bundle">
    End-to-end headless walkthrough for fixed-price sets.
  </Card>

  <Card title="Dynamic pricing bundle guide" icon="boxes-stacked" href="/bundles/dynamic-pricing-bundle">
    End-to-end headless walkthrough for build-your-own discounts.
  </Card>

  <Card title="Volume discount bundle guide" icon="layer-group" href="/bundles/volume-discount-bundle">
    End-to-end headless walkthrough for tiered savings.
  </Card>

  <Card title="Authentication" icon="key" href="/bundles/authentication">
    For backend-only reads — never used from a storefront.
  </Card>
</CardGroup>
