> ## 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.

# Sectioned bundle with a custom storefront

> Sell a box of sections, each with its own minimum and maximum, as a one-time or subscription purchase from your own storefront UI: read the rule, add each pick with Appstle's bundle attributes and a selling plan, and let the automatic discount and Appstle Subscriptions price the box.

A **sectioned bundle** is a build-a-box offer split into sections. Each section lists the products a shopper can pick from and has its own minimum and maximum, such as *"Pick 2 to 3 single-origin coffees and up to 1 extra, and save more the more you pick"*. The merchant discounts the box by tiers that count its units, or by one percentage or amount off the whole box.

This guide shows how to run a sectioned bundle from your own storefront UI, without the Appstle widget. You use the same rule data, cart-line attributes and Shopify Function as the built-in widget, so only the UI is yours. If you haven't read the [headless overview](https://developers.appstle.com/bundles/headless-overview.md), start there: it explains the automatic-discount model these steps rely on.

| | |
| - | - |
| **`bundleType`** | `SECTIONED_BUNDLE` |
| **`bundleSubType`** | Not set (`null`) |
| **Read from** | `appstle_bundles.bundle_rules` shop metafield |
| **Discount applied by** | Appstle automatic discount (Shopify Function) |
| **Purchase type** | One-time, subscription or both, set by the rule's `appliesOn` |

> [!NOTE]
>
> The merchant creates a sectioned bundle in the Appstle admin: in **Bundles & Discounts**, click **Create offer**, then choose **Sectioned bundle**. There is no API to create one.

## How pricing and limits work

A sectioned rule's `discountType` sets how the box is discounted:

| `discountType` | How the box is discounted |
| - | - |
| `TIERED_DISCOUNT` | By the tiers in `tieredDiscount`. A tier applies when the box reaches its `value`, and the box gets the reached tier that saves the most. |
| `PERCENTAGE` | By `discountValue` percent, once the box is within the rule's own `minProductCount`, `maxProductCount`, `minOrderAmount` and `maxOrderAmount`. |
| `FIXED_AMOUNT` | By `discountValue` off the whole box, within the same rule limits. |
| `NO_DISCOUNT` | Not discounted. The box only groups the shopper's picks. |

Each tier's `discountBasedOn` says what its `value` counts:

| `discountBasedOn` | `value` means |
| - | - |
| `QUANTITY` | The number of counted units in the box. |
| `AMOUNT` | The total price of the counted units, before the box discount. The merchant sets it in the store's currency, and it's converted for carts in another currency. |

A tier's own `discountType` is `PERCENTAGE` (that percentage off each counted line) or `FIXED_AMOUNT` (that amount off the box, spread across its counted lines).

This guide's examples use a `TIERED_DISCOUNT` rule with `QUANTITY` tiers: 2 units save 5%, 3 save 10% and 4 save 15%.

Each section has `minProductCount` and `maxProductCount`. The Function applies them like this:

* **Minimum.** A box gets no discount until **every** section has at least its `minProductCount` units.
* **Maximum.** Units above a section's `maxProductCount` don't count towards the tier and don't get a discount.
* **Membership.** A cart line belongs to the first section whose `variants` list contains the line's variant. The Function never looks at which section your UI showed the product in. A line whose variant is in no section doesn't count and isn't discounted, even with the box attributes.

The discount applies automatically, in the cart and at checkout, when the cart holds the box's lines with the attributes in [Step 3](#step-3-—-add-the-box-to-the-cart). You don't compute or submit it.

> [!WARNING]
>
> The Function trims over-limit units for you, but it can't tell the shopper why. Enforce each section's minimum and maximum in your own UI before you add anything to the cart, so the shopper never pays full price for a unit they expected to be discounted.

## Step 1 — Read the rule from the metafield

Read the rule from the shop metafield, exactly as the [headless overview](https://developers.appstle.com/bundles/headless-overview.md#reading-bundle-configuration) shows (Liquid on a theme, the Shopify Admin API on a headless stack), and keep the rule whose `bundleType` is `SECTIONED_BUNDLE` and `status` is `ACTIVE`:

```js theme={null}
const rule = (window.MY_BUNDLE_RULES || []).find(
  (r) => r.bundleType === 'SECTIONED_BUNDLE' && r.status === 'ACTIVE' && r.uniqueRef === 'coffee-set-ref'
);
const sections = JSON.parse(rule.sections);
const tiers = JSON.parse(rule.tieredDiscount);
```

`sections`, `tieredDiscount` and `settings` are **JSON-encoded strings** inside the rule, so parse them before use. This is the live rule for the **Build your set** bundle, trimmed to the fields this guide uses:

```json theme={null}
{
  "id": 10001,
  "name": "Build your set",
  "status": "ACTIVE",
  "bundleType": "SECTIONED_BUNDLE",
  "uniqueRef": "coffee-set-ref",
  "discountType": "TIERED_DISCOUNT",
  "appliesOn": "BOTH",
  "sellingPlanType": "BUNDLE_LEVEL",
  "tieredDiscount": "[{\"discountBasedOn\":\"QUANTITY\",\"value\":2,\"discount\":5,\"discountType\":\"PERCENTAGE\"},{\"discountBasedOn\":\"QUANTITY\",\"value\":3,\"discount\":10,\"discountType\":\"PERCENTAGE\"},{\"discountBasedOn\":\"QUANTITY\",\"value\":4,\"discount\":15,\"discountType\":\"PERCENTAGE\"}]",
  "sections": "[{\"id\":1,\"name\":\"Single origins\",\"description\":\"\",\"minProductCount\":2,\"maxProductCount\":3,\"variants\":[...],\"imageURL\":null}, ...]"
}
```

After `JSON.parse`, the first section looks like this (one variant shown):

```json theme={null}
{
  "id": 1,
  "name": "Single origins",
  "description": "",
  "minProductCount": 2,
  "maxProductCount": 3,
  "variants": [
    {
      "productId": 8000000000011,
      "variantId": 44000000000011,
      "name": "Colombia Coffee",
      "productHandle": "colombia-coffee",
      "imageSrc": "https://cdn.shopify.com/.../colombia.png",
      "price": "20.00",
      "status": "ACTIVE",
      "productTitle": "Colombia Coffee",
      "variantTitle": null,
      "isMandatory": false,
      "preSelected": false
    }
  ]
}
```

| Field | Meaning |
| - | - |
| `uniqueRef` | Attach it to every line of the box as `_appstle-bb-id`. |
| `name`, `discountName` | The box's name. The cart's discount allocation shows `discountName`, or `name` when `discountName` is empty. |
| `discountType`, `discountValue` | How the box is discounted. See [How pricing and limits work](#how-pricing-and-limits-work). |
| `tieredDiscount[]` | The tiers: `discountBasedOn`, `value` (the threshold), `discount` and `discountType`. A tier with `discountAllowedTags` applies only to customers with one of those tags. |
| `minProductCount`, `maxProductCount`, `minOrderAmount`, `maxOrderAmount` | Limits on the whole box, used by `PERCENTAGE` and `FIXED_AMOUNT` rules. |
| `sections[].id` | The section's identifier, unique within the rule. |
| `sections[].name`, `description` | Text for your section heading. |
| `sections[].minProductCount`, `maxProductCount` | The section's limits, in units. `0` means no limit. |
| `sections[].variants[]` | The variants the shopper can pick in this section. Hydrate titles, images and prices from Shopify, as the overview describes. |
| `sections[].variants[].isMandatory` | The box gets no discount unless this variant is in it. |
| `sections[].variants[].preSelected` | The merchant wants this variant picked when the box opens. |
| `appliesOn` | `ONE_TIME`, `SUBSCRIPTION` or `BOTH`. See [Step 4](#step-4-—-sell-the-box-on-a-subscription). |
| `sellingPlanType` | `BUNDLE_LEVEL`: the shopper picks one plan for the whole box. `PRODUCT_LEVEL`: one plan for each product. |
| `hideSubscriptionPurchase` | When `true`, sell the box one-time only. |
| `settings` | A JSON-encoded string of display and purchase options, such as `includedSubscriptionPlans`, `excludeSubscriptionPlans`, `enableSectionPurchaseOptions` and `excludeB2BCustomers`. |

## Step 2 — Render the sections

Build one picker per section from `sections[].variants`, and enforce the limits as the shopper picks:

* Stop adding to a section when its units reach `maxProductCount`.
* Keep the **Add to cart** button disabled until every section has at least `minProductCount` units.
* Show the tier the box has reached and the next one, from `tieredDiscount`. A client-side preview is a number you compute from the same fields; the Function decides the real discount, in the cart and at checkout.

## Step 3 — Add the box to the cart

Add each picked variant as its own cart line, with these line properties (`attributes` in Shopify's Storefront API):

| Attribute key | Value | Notes |
| - | - | - |
| `_appstle-bb-id` | The rule's `uniqueRef` | **Required.** Without it, no discount applies. |
| `_appstle_bundle_instance_id` | A new UUID for each box | Groups the lines of one box. Send the same value on every line of a box, and a new one each time the shopper adds another box. Without it, every line of the rule counts as one box. |
| `_appstle_bundles_type` | `SECTIONED_BUNDLE` | The discount Function doesn't read it, but Appstle Subscriptions does. Always send it: without it, the box discount isn't converted into the contract's `BUILD_A_BOX_DISCOUNT_<uniqueRef>` at checkout, and it isn't re-priced when a subscriber edits the box ([after checkout](#after-checkout-the-subscription)). |
| `__appstle-bb-name` | The rule's `name` | A label for your own cart UI. Nothing reads it for the discount. |
| `_appstle-bundles-variants` | The line's numeric variant ID | Set by the widget and kept on the subscription line. The discount doesn't read it. |
| `_appstle_bundles_subscription_selling_plan_id` | The numeric ID of the selling plan on the line | Subscription lines only. Set by the widget and kept on the subscription line. |

> [!NOTE]
>
> If a variant in `sections[].variants` has `isMandatory: true`, the box gets no discount unless that variant is in it. The widget also sends `_appstle_bundles_added_to_cart_at` and `_appstle_bundle_mandatory_products`. You don't need either.

Keys are matched exactly, including the mix of hyphens and underscores, so copy them verbatim. Keys that begin with `_` stay hidden from the shopper in standard Shopify cart and checkout UIs.

```js Shopify theme (Ajax Cart API) theme={null}
const instanceId = crypto.randomUUID(); // one per box

const res = await fetch(`${Shopify.routes.root}cart/add.js`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: picks.map((p) => ({
      id: p.variantId,        // numeric Shopify variant ID
      quantity: p.quantity,
      properties: {
        '_appstle-bb-id': rule.uniqueRef,
        '_appstle_bundle_instance_id': instanceId,
        '_appstle_bundles_type': 'SECTIONED_BUNDLE',
        '__appstle-bb-name': rule.name,
        '_appstle-bundles-variants': String(p.variantId),
      },
    })),
  }),
});
if (!res.ok) {
  const { description } = await res.json(); // show this to the shopper, then re-read /cart.js
}
```

```graphql Headless (Shopify Storefront API) theme={null}
mutation AddSectionedBox($cartId: ID!, $instanceId: String!) {
  cartLinesAdd(
    cartId: $cartId
    lines: [
      {
        merchandiseId: "gid://shopify/ProductVariant/44000000000011"
        quantity: 2
        attributes: [
          { key: "_appstle-bb-id", value: "coffee-set-ref" }
          { key: "_appstle_bundle_instance_id", value: $instanceId }
          { key: "_appstle_bundles_type", value: "SECTIONED_BUNDLE" }
          { key: "__appstle-bb-name", value: "Build your set" }
          { key: "_appstle-bundles-variants", value: "44000000000011" }
        ]
      }
      {
        merchandiseId: "gid://shopify/ProductVariant/44000000000012"
        quantity: 1
        attributes: [
          { key: "_appstle-bb-id", value: "coffee-set-ref" }
          { key: "_appstle_bundle_instance_id", value: $instanceId }
          { key: "_appstle_bundles_type", value: "SECTIONED_BUNDLE" }
          { key: "__appstle-bb-name", value: "Build your set" }
          { key: "_appstle-bundles-variants", value: "44000000000012" }
        ]
      }
    ]
  ) {
    cart { id }
    userErrors { field message }
  }
}
```

On a headless storefront, create the cart with `cartCreate` first, then pass its `id` as `$cartId` and the box's UUID as `$instanceId`. Send one entry in `lines` for each pick, each with the same five attributes, and treat the add as done only when `userErrors` is empty. The call needs a Storefront API access token: see Shopify's [Storefront API documentation](https://shopify.dev/docs/api/storefront). The [overview](https://developers.appstle.com/bundles/headless-overview.md#adding-to-cart-on-a-fully-headless-stack-shopify-storefront-api) has another `cartLinesAdd` example.

## Step 4 — Sell the box on a subscription

A line is a subscription line when it has a selling plan: `selling_plan` in the Ajax Cart API, `sellingPlanId` in Shopify's Storefront API. When the order is placed, Appstle Subscriptions creates the contract. The bundle rule doesn't replace the selling plan.

The rule's `appliesOn` decides which lines get the box discount:

| `appliesOn` | What your UI offers | Lines that get the box discount |
| - | - | - |
| `ONE_TIME` | One-time only. | Lines without a selling plan. |
| `SUBSCRIPTION` | Subscription only. | Lines on a selling plan. |
| `BOTH` | Either, as the shopper chooses. | Every line. |

In a live test with a `SUBSCRIPTION` rule, a box of two subscription lines (3 units) and one one-time line (1 unit) discounted only the subscription lines. The one-time line stayed at full price, but its unit still counted, so the box reached the 15% tier.

1. Offer only the purchase types `appliesOn` allows, and only one-time when `hideSubscriptionPurchase` is `true`.
2. Take the plans from the products' own selling plans: from `selling_plan_allocations`, or from `selling_plan_groups` in `/products/<handle>.js`. A choice that covers several products can offer only the plans attached to all of them. If the rule's `settings` has `includedSubscriptionPlans` or `excludeSubscriptionPlans` (comma-separated selling plan IDs), offer only the included plans and none of the excluded ones.
3. Ask for one plan for the whole box when `sellingPlanType` is `BUNDLE_LEVEL`, one per product when it's `PRODUCT_LEVEL`, and one per section when `settings` has `enableSectionPurchaseOptions: true`.
4. Add every line of the box with the plan the shopper chose for it, and record the plan in the line properties:

```js theme={null}
items: picks.map((p) => ({
  id: p.variantId,
  quantity: p.quantity,
  selling_plan: p.sellingPlanId, // omit for a one-time line
  properties: {
    '_appstle-bb-id': rule.uniqueRef,
    '_appstle_bundle_instance_id': instanceId,
    '_appstle_bundles_type': 'SECTIONED_BUNDLE',
    '__appstle-bb-name': rule.name,
    '_appstle-bundles-variants': String(p.variantId),
    '_appstle_bundles_subscription_selling_plan_id': p.sellingPlanId?.toString(),
  },
}))
```

A selling plan belongs to the products it's attached to. If a line's plan isn't attached to its variant, `/cart/add.js` returns `422`:

```json theme={null}
{ "status": 422, "message": "Cart Error", "description": "Cannot apply selling plan to variant" }
```

> [!WARNING]
>
> A failed `cart/add.js` request can leave part of the box in the cart. After a `422`, read `/cart.js` and remove the box's lines before you retry.

The box discount applies on top of the plan's own price. In a live test, an 18.00 coffee on a monthly plan that takes 10% off costs 16.20, and the 15% box tier takes it to 13.77.

## Step 5 — The discount applies automatically

Once the lines are in the cart, the Function discounts every eligible line. Confirm it on `/cart.js`: each discounted line carries a discount allocation titled with the rule's `discountName`, or with its `name` when `discountName` is empty (`Build your set` here).

This is the real cart for a box of 2 Colombia and 1 Dark Roast (section **Single origins**, 3 of 3) and 1 Decaf (section **Extras**). Four units reach the 15% tier, and each line's discount allocation is titled `Build your set`. Prices are in dollars here, and `/cart.js` returns cents:

| Line | Before | After | Discount |
| - | - | - | - |
| Decaf Coffee × 1 | 18.00 | 15.30 | 2.70 |
| Dark Roast Coffee × 1 | 18.00 | 15.30 | 2.70 |
| Colombia Coffee × 2 | 40.00 | 34.00 | 6.00 |

The cart total goes from 76.00 to 64.60.

These are the cases to design for. Each one was run on the demo store:

| Cart | Result |
| - | - |
| Section minimum not met (1 unit in a section with a minimum of 2) | No discount. |
| Only the first section has units, at its minimum of 2 | The 5% tier. A section with a minimum of 0 doesn't need units. |
| Section maximum exceeded (4 units in a section with a maximum of 3) | Only 3 units count, so the 10% tier applies. Shopify splits one line into a discounted line and a full-price line. |
| Extras section exceeded (2 units in a section with a maximum of 1) | Only one of the two **Extras** units is discounted. With 2 units in **Single origins**, the box counts 3 units and gets the 10% tier. |
| Two boxes with different `_appstle_bundle_instance_id` values | Each box is priced on its own. |
| No attributes (no `_appstle-bb-id`) | No discount. |
| No `_appstle_bundles_type` | Still discounted in the cart. Appstle Subscriptions needs it to keep the box discount on the contract and to re-price the box after an edit. |
| A subscription line | Discounted on top of the plan's price. Under a `BOTH` rule, one-time and subscription lines in one box are both discounted. |

> [!NOTE]
>
> A box also stays undiscounted when:
>
> * The rule isn't `ACTIVE`.
> * A section is under its `minProductCount`, or a variant marked `isMandatory` isn't in the box.
> * The rule is limited by customer tags (`allowedTags`, `restrictTags`) or by uses per customer (`limitToUsePerCustomer`), and the cart's buyer isn't identified or doesn't qualify.
> * The buyer is purchasing for a Shopify B2B company and the rule's `settings` has `excludeB2BCustomers: true`.
>
> A tier with `discountAllowedTags` applies only to a signed-in customer who has one of those tags.

> [!TIP]
>
> Don't rely on *which* units lose the discount when a section is over its maximum. Enforce the limits in your UI.

## After checkout: the subscription

When the order has a subscription line, Appstle Subscriptions creates the contract, and the box discount comes with it.

**Every line keeps your attributes.** Each contract line carries the properties you sent as `customAttributes`. This is a line from the live contract, read with the Subscriptions [Admin API](https://developers.appstle.com/subscription/authentication.md) (`GET /api/external/v2/subscription-contracts/contract-external/{contractId}`), trimmed:

```json theme={null}
{
  "title": "Dark Roast Coffee",
  "quantity": 1,
  "sellingPlanId": "gid://shopify/SellingPlan/690000000001",
  "customAttributes": [
    { "key": "_appstle-bb-id", "value": "coffee-set-ref" },
    { "key": "_appstle_bundles_type", "value": "SECTIONED_BUNDLE" },
    { "key": "__appstle-bb-name", "value": "Build your set" },
    { "key": "_appstle-bundles-variants", "value": "44000000000012" },
    { "key": "_appstle_bundles_subscription_selling_plan_id", "value": "690000000001" },
    { "key": "_appstle_bundle_instance_id", "value": "44444444-4444-4444-8444-444444444444" }
  ]
}
```

**The contract carries the box discount.** It's a manual discount named `BUILD_A_BOX_DISCOUNT_<uniqueRef>`. This rule doesn't limit how many orders get the discount, so `recurringCycleLimit` is `null` and the discount applies to every renewal. When the merchant limits the discount to a number of orders, the first order counts as one of them:

```json theme={null}
{
  "title": "BUILD_A_BOX_DISCOUNT_coffee-set-ref",
  "value": { "percentage": 15 },
  "recurringCycleLimit": null,
  "targetType": "LINE_ITEM",
  "type": "MANUAL"
}
```

### When the subscriber edits the box

When a box is edited after checkout, in the customer portal or by your own code calling the [Subscriptions Storefront API](https://developers.appstle.com/subscription/build-your-own-frontend.md#managing-line-items), Appstle Subscriptions re-prices it in the same request. It reads the rule from Appstle Bundles, counts the box's units, picks the tier and replaces the discount with one named `SECTIONED_DISCOUNT_<uniqueRef>`. The new discount is on the contract when the edit call returns.

Re-pricing treats every line of the same bundle on a contract as one box, whatever its `_appstle_bundle_instance_id`, so two boxes bought in one order are priced together after an edit. If the merchant has turned the offer off, an edit leaves the contract's box discount as it is.

These are real results on the demo contract, one edit after another:

| Edit | Units in the box | Contract discount |
| - | - | - |
| Remove a 2-unit line from a box of 6 (2 units over the **Single origins** maximum) | 4 | 15% |
| Remove a line of 1 unit | 3 | 10% |
| Set a line's quantity from 2 to 1 | 2 | 5% |
| Add a 3-unit line to **Single origins**, taking it to 4 units (maximum 3) | 5 | 15% |
| Remove the **Extras** line, leaving 4 units in **Single origins** (maximum 3) | 4 | 15%: the unit over the maximum still counts |

> [!WARNING]
>
> Appstle Bundles has to be connected to Appstle Subscriptions for the discount to be re-priced. If the connection is missing, an edit **removes the old box discount and applies no new one**. Ask the merchant to reconnect it in the Bundles admin: **Integrations**, then **Appstle Subscriptions**.

### Section limits on a subscription

* Appstle's customer portal checks each section's minimum and maximum when a subscriber adds items to the box.
* The portal **API** doesn't. In live tests, adding a line that took a section past its maximum returned `200`, and so did setting a quantity that left a section under its minimum.
* When Appstle Subscriptions re-prices, it counts the units on every contract line whose variant is in one of the rule's sections. It doesn't trim a section to its maximum, and it can keep a tier when a section is under its minimum: in a live test, a box with 1 unit in a section with a minimum of 2 kept the 5% tier.

If you build your own subscriber portal, enforce `minProductCount` and `maxProductCount` there, the same way as in [Step 2](#step-2-—-render-the-sections), before you call the API.

To add items to a box from your own subscriber portal, call the Subscriptions Storefront API on your storefront domain, as the signed-in customer:

```
PUT /apps/subscriptions/cp/api/v2/subscription-contracts-add-sectioned-line-item/{contractId}
```

```json theme={null}
{
  "bundleUniqueId": "coffee-set-ref",
  "sectionedBundleItems": [
    { "id": 1, "sectionedVariantInfos": [{ "variantId": "44000000000012", "variantQty": 2 }] }
  ]
}
```

| Field | Meaning |
| - | - |
| `sectionedBundleItems[].sectionedVariantInfos[].variantId` | The variant to add, as a numeric ID in a string. |
| `sectionedBundleItems[].sectionedVariantInfos[].variantQty` | The quantity to add. |
| `bundleUniqueId`, `sectionedBundleItems[].id` | The rule's `uniqueRef` and the section's `id`. Send them, but they aren't checked: the line joins the box already on the contract, and its variant decides its section. |

The endpoint adds each variant as its own subscription line, then re-prices the box. It returns `200` with a JSON array that holds the contract after each added variant, so the last element is the contract's current state. See [Add a line item to a specific section of a contract](https://developers.appstle.com/subscription-storefront-api/subscription-contracts/add-a-line-item-to-a-specific-section-of-a-contract.md) in the API reference.

For a signed-in customer, the call refuses the add with a `400` when any of these checks fail. The body is `application/problem+json`, and its `title` holds the message:

| The call needs | `title` when it fails |
| - | - |
| The contract belongs to the signed-in customer | `Contract does not exist` |
| The subscription isn't a gift subscription | `UserGeneratedError:This is a gift subscription, so it can't be changed here. Please contact us if anything needs to change.` |
| The store lets customers add products: **Customer Portal → Customize portal → Features → Products → Customers can add products to a subscription** in Appstle Subscriptions | `UserGeneratedError:Permission denied, Subscription cannot be modified` |
| On a prepaid subscription that ships as separate orders, the store lets customers change products: **Customers can change products on prepaid (separate orders) subscriptions** | `UserGeneratedError:Permission denied, Subscription cannot be modified` |
| The subscription has fewer than 50 lines (the default limit) | `UserGeneratedError: A subscription cannot contain more than 50 distinct products. Currently, there are 50 products.` |

Show the gift message to the customer without the `UserGeneratedError:` prefix. `Permission denied, Subscription cannot be modified` means the store doesn't let this customer add to the box, so hide adding on that subscription. The variants are added one at a time, so a call that fails part-way, such as when the subscription reaches its product limit (50 by default), can leave the earlier variants on the subscription: read the contract again before you retry.

A call without a signed-in customer answers `401` (see [Authentication](https://developers.appstle.com/subscription/build-your-own-frontend.md#authentication)), and a call over a [rate limit](https://developers.appstle.com/subscription/build-your-own-frontend.md#rate-limits) answers `429` with a `Retry-After` header: wait that many seconds before you retry.

Any product added to a subscription that holds a sectioned box, through this endpoint or [`subscription-contracts-add-line-item`](https://developers.appstle.com/subscription/build-your-own-frontend#managing-line-items), copies the box's attributes from the contract's box lines, with `_appstle-bundles-variants` and `_appstle_bundles_subscription_selling_plan_id` set to the new line's variant and plan. A product that isn't in one of the rule's sections gets the attributes too, but it doesn't count towards the box. Lines added after checkout store those two values as Shopify IDs (`gid://shopify/ProductVariant/...`, `gid://shopify/SellingPlan/...`), where checkout lines store plain numbers, so normalize them when you read them.

## Checklist

**1. The offer is live in the Appstle admin**

Only live offers (`status` is `ACTIVE`) have an automatic discount.

**2. Your UI enforces every section's limits**

The Function trims what exceeds a maximum and withholds the discount below a minimum, but your shoppers need to know why.

**3. Every line carries the box attributes**

The same `_appstle-bb-id` and `_appstle_bundle_instance_id` on each line of a box, plus `_appstle_bundles_type` set to `SECTIONED_BUNDLE`.

**4. Subscription lines use a selling plan attached to the variant**

Each line has its own `selling_plan`, and the plan's ID is also in `_appstle_bundles_subscription_selling_plan_id`.

**5. Purchase types match appliesOn**

Put the lines on a selling plan for a `SUBSCRIPTION` rule, and none for `ONE_TIME`.

**6. Verify the discount in the cart**

Check `/cart.js` for the discount allocation on each line.

**7. Appstle Bundles is connected to Appstle Subscriptions**

Needed to re-price a subscription's box discount after an edit.

## Next steps

> [card] [Build your own frontend](https://developers.appstle.com/bundles/headless-overview.md)
>
> How the metafield, attributes and discount Function fit together.

> [card] [Combo bundle](https://developers.appstle.com/bundles/combo-bundle.md)
>
> Sell a fixed set of products with a per-product discount.

> [card] [Build your own subscription frontend](https://developers.appstle.com/subscription/build-your-own-frontend.md)
>
> Build the customer portal that edits subscriptions.

> [card] [JavaScript events and API](https://developers.appstle.com/bundles/javascript-events.md)
>
> The globals, methods and events your theme code can use.


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