> ## 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 a Dynamic Pricing Build-a-Box with a custom UI

> Let customers pick products on your own page, add them to the cart as one subscription, and get the box's tier discount code to apply at checkout.

A Dynamic Pricing Build-a-Box lets a customer pick several products from a list. Each pick is an ordinary subscription line at the product's own price, and the box's tiers take a percentage off when the customer picks enough items or spends enough. Appstle's own box page does all of this for you. This guide shows how to do the same from your own UI: read the box, add the picks to the cart, ask Appstle for the discount code that matches the cart, and send the customer to checkout with it.

> [!NOTE]
>
> Steps 1, 2 and 4 run in the customer's browser. Step 1 calls Appstle's **Storefront API** through Shopify's App Proxy, so it needs no API key; it isn't Shopify's Storefront GraphQL API. Step 2 is Shopify's Cart API. In Step 3 you choose: call Appstle's Storefront API from the browser, or call the **Admin API** from your server with an API key. Never put an API key in code that runs in the browser. If the store changed its App Proxy path, replace `/apps/subscriptions` with it. Read [Build your own frontend](https://developers.appstle.com/subscription/build-your-own-frontend.md) first if you haven't used the App Proxy before.

## What you'll build

* A page that reads a box's rules and choices and lets the customer pick products.
* A cart add that puts each pick in the cart with a selling plan.
* A call that returns the discount code for the cart, or tells you that no discount applies.
* A redirect to checkout with the code applied.

## Before you start

* A Dynamic Pricing box (a **Dynamic Pricing Bundle**) with its status set to **Enabled** in the **Build-A-Box** section of the Appstle Subscriptions app, and at least one tiered discount. Note its reference: the box list shows it as **Ref**, and the API calls it `uniqueRef`. A box that is disabled, or a reference that matches no box, is treated as having no discount (see [Errors](#errors)).
* A subscription plan on every product the customer can pick. The plans must have matching delivery frequencies.
* For Step 3 from your server, an Admin API key with **Read & Write** access. See [Authentication](https://developers.appstle.com/subscription/authentication.md).

## How the tiers work

The box's tiers are in `bundle.tieredDiscount`, a JSON string. For a box with one tier it looks like this:

```json theme={null}
[{ "discountBasedOn": "QUANTITY", "quantity": 3, "discount": "10" }]
```

| Field | Meaning |
| - | - |
| `discountBasedOn` | `QUANTITY`: the tier applies when the cart holds at least `quantity` items. `AMOUNT`: it applies when the cart total is at least `quantity` in the store's currency, such as 50 for \$50.00. |
| `quantity` | The threshold, a whole number. |
| `discount` | The percentage off, from 0 to 100. It can be a number or a string, so convert it with `Number()`. |

When the cart meets several tiers, Appstle uses the one with the largest discount. When it meets none, there's no discount and no code.

## Step 1: Read the box

Get the box's rules and its choices. The call goes through the App Proxy, so it needs no API key:

```javascript theme={null}
const ref = 'xreuziyr'; // the box's uniqueRef
const response = await fetch(
  `/apps/subscriptions/bb/api/v3/subscription-bundlings/external/get-bundle/${ref}`
);
if (response.status !== 200) throw new Error(`Box ${ref} is unavailable (${response.status})`);
const box = await response.json();
const tiers = JSON.parse(box.bundle.tieredDiscount || '[]');
const plans = JSON.parse(box.subscription.infoJson).subscriptionPlans;
```

The reference lists the same call under `/apps/subscriptions/cp/api/`, which needs a signed-in customer: see [Get public Build-a-Box bundle details by handle (v3)](https://developers.appstle.com/subscription-storefront-api/build-a-box-&-bundles/get-public-build-a-box-bundle-details-by-handle-v3.md). The call answers `204` with an empty body when no box has this reference or the box is disabled.

A trimmed response for a box named Coffee Box:

```json theme={null}
{
  "bundle": {
    "name": "Coffee Box",
    "uniqueRef": "xreuziyr",
    "subscriptionBundlingEnabled": true,
    "minProductCount": 2,
    "maxProductCount": 6,
    "tieredDiscount": "[{\"discountBasedOn\":\"QUANTITY\",\"quantity\":3,\"discount\":\"10\"}]",
    "bundleRedirect": "CHECKOUT"
  },
  "subscription": {
    "groupName": "Coffee Subscription",
    "infoJson": "{\"subscriptionPlans\":[{\"id\":\"gid://shopify/SellingPlan/692928545072\",\"frequencyName\":\"Monthly Subscription\"}]}"
  },
  "products": [
    { "productId": 15386633601328, "productTitle": "House Blend Coffee", "productHandle": "house-blend-coffee" },
    { "productId": 15386744946992, "productTitle": "Dark Roast Coffee", "productHandle": "dark-roast-coffee" }
  ]
}
```

| Field | Meaning |
| - | - |
| `bundle.minProductCount`, `bundle.maxProductCount` | The fewest and most items Appstle's box page lets a customer pick in total. Shopify's cart doesn't know about them and the discount call doesn't reject a cart outside them, so your UI must enforce them. |
| `bundle.tieredDiscount` | The tiers, as a JSON string. See [How the tiers work](#how-the-tiers-work). |
| `subscription.infoJson` | The box's subscription plan group, as a JSON string. Each entry of `subscriptionPlans` has the selling plan's `id` (a `gid://shopify/SellingPlan/...` value) and its `frequencyName`. |
| `products` | The products the customer can pick from. Read each one's variants from Shopify's product JSON, `/products/<productHandle>.js`. |

## Step 2: Add the picks to the cart

Add each pick as its own line, with the variant, the quantity and the selling plan. This is Shopify's Cart API, so no Appstle call is involved:

```javascript theme={null}
async function addPicksToCart(picks, sellingPlanId) {
  // picks: [{ variantId, quantity }, ...]
  const response = await fetch('/cart/add.js', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      items: picks.map((p) => ({
        id: p.variantId,
        quantity: p.quantity,
        selling_plan: sellingPlanId,
      })),
    }),
  });
  if (!response.ok) throw new Error(`Cart add failed: ${response.status}`);
  return response.json();
}
```

These are the three fields Appstle's own box page sends. Every line in the cart counts toward the tiers, including products that aren't in the box, so start from an empty cart: call Shopify's `POST /cart/clear.js` first.

> [!WARNING]
>
> Shopify answers `422` with `Cannot apply selling plan to variant` when the selling plan isn't attached to the variant's product. Take the plan from the box's own plan group, as in Step 1.

## Step 3: Get the discount code

Read the cart back from Shopify, and send it to Appstle with the box's reference. The cart is the JSON that Shopify's `GET /cart.js` returns, with Shopify's own snake\_case field names.

```javascript theme={null}
const cart = await (await fetch('/cart.js')).json();
const response = await fetch(`/apps/subscriptions/bb/api/subscription-bundlings/discount/${ref}`, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ cart }),
});
if (!response.ok) throw new Error(`Discount call failed: ${response.status}`);
const text = await response.text();
const result = text ? JSON.parse(text) : { discountNeeded: false };
```

From your server, the Admin API has the same call: [Generate discount code for bundle](https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/generate-discount-code-for-bundle.md). It uses an API key, so run it on your server and pass it the cart your page sends you:

```bash theme={null}
curl -X PUT "https://subscription-admin.appstle.com/api/external/v2/subscription-bundlings/discount/xreuziyr" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cart": {
      "total_price": 5400,
      "items": [
        { "product_id": 15386744946992, "variant_id": 68185382945072, "quantity": 3 }
      ]
    }
  }'
```

Appstle reads these cart fields and ignores the rest:

| Field | Type | Description |
| - | - | - |
| `cart.total_price` | integer | Required. The cart total in the smallest currency unit, such as cents (`5400` is \$54.00). It decides `AMOUNT` tiers. |
| `cart.items` | array | Required. One entry per cart line. |
| `cart.items[].quantity` | integer | Required. Adds up to the item count that decides `QUANTITY` tiers. |
| `cart.items[].product_id` | integer | The line's product. The discount covers the products in the cart that belong to the box. |
| `cart.items[].variant_id` | integer | The line's variant. |
| `cart.items[].properties` | object | Optional. When any line carries the property `_appstle-bb-id`, only those lines count toward the tiers and the discount. |

> [!WARNING]
>
> Use the snake\_case names above. A name Appstle doesn't know, such as `totalPrice` or `productId`, is ignored without an error. With `total_price` missing, the call fails with the server error described in [Errors](#errors). With `product_id` and `variant_id` misnamed, the call returns `200` with an empty body and no code.

A cart that meets a tier returns the code:

```json theme={null}
{
  "discountCode": "BUILD_A_BOX_DISCOUNT_BptcBVDDOP_xreuziyr",
  "discountNeeded": true
}
```

A cart that meets none returns:

```json theme={null}
{
  "discountNeeded": false
}
```

| Field | Description |
| - | - |
| `discountNeeded` | `true` when the cart earns a discount and `discountCode` holds its code. `false` when it doesn't, and no code is created. |
| `discountCode` | The code, `BUILD_A_BOX_DISCOUNT_<ten letters>_<uniqueRef>`. Only present when `discountNeeded` is `true`. |

Every call that returns a code creates a new discount in the store's Shopify admin, so call once, when the customer goes to checkout, and not on every change to the cart. If the customer changes the cart after that, ask for a code again. The code gives the matched tier's percentage off the box's products in the cart. It applies to subscription purchases, stays on renewal orders, needs at least the tier's quantity or subtotal, and combines with product, order and shipping discounts. It allows a one-time purchase only when the box does.

## Step 4: Send the customer to checkout

Use Shopify's discount link, which applies the code and then opens the page you name. It is the link Appstle's own box page uses. When `discountNeeded` is `false`, go straight to checkout:

```javascript theme={null}
window.location.href = result.discountNeeded
  ? `/discount/${encodeURIComponent(result.discountCode)}?redirect=/checkout`
  : '/checkout';
```

Use `?redirect=/cart` to show the cart first. When the customer pays, Shopify creates the subscription with one line for each pick.

## Errors

| Response | What it means | What to do |
| - | - | - |
| `200` with `{"discountNeeded": false}` | The cart meets no tier, or the reference matches no box, or the box is disabled. | Go to checkout without a code. |
| `200` with an empty body | Appstle couldn't create the discount in Shopify. | Treat it as no discount, and check the field names in the request. |
| `400` with `"status": 500` and `"message": "error.http.500"` | The request is missing `cart`, `cart.items`, an item's `quantity` or `cart.total_price`, often because it uses camelCase names such as `totalPrice`. The title reads `Internal Server Error` and the detail suggests trying again later. | Don't retry: the same request fails every time. Fix the field names. |
| `401` | The Admin API call has no valid API key. | Check the `X-API-Key` header. |
| `403` | The Admin API key is read-only. | Use a **Read & Write** key. |

## Next steps

> [card] [Fixed-price Build-a-Box](https://developers.appstle.com/subscription/fixed-price-box-custom-ui.md)
>
> The same flow for a box with one price and customer-chosen contents.

> [card] [Build your own frontend](https://developers.appstle.com/subscription/build-your-own-frontend.md)
>
> Selling plans, the cart and the customer portal from your own UI.

> [card] [Authentication](https://developers.appstle.com/subscription/authentication.md)
>
> Admin API keys and how storefront calls are authenticated.

> [card] [Integration guide](https://developers.appstle.com/subscription/integration-guide.md)
>
> Server-side calls for subscriptions and contracts.


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