Skip to main content
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, start there: it explains the automatic-discount model these steps rely on.
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: Each tier’s discountBasedOn says what its value counts: 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. You don’t compute or submit it.
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 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:
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:
After JSON.parse, the first section looks like this (one variant shown):

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):
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.
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. The overview 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: 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:
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:
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: 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:
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.
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 (GET /api/external/v2/subscription-contracts/contract-external/{contractId}), trimmed:
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:

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, 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:
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, 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:
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 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: 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), and a call over a rate limit 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, 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

Build your own frontend

How the metafield, attributes and discount Function fit together.

Combo bundle

Sell a fixed set of products with a per-product discount.

Build your own subscription frontend

Build the customer portal that edits subscriptions.

JavaScript events and API

The globals, methods and events your theme code can use.