---
title: Edit subscription contracts with bundles
description: >-
  Learn how bundle products are represented on a subscription contract, and how
  to add, update, and remove bundles with the SubscriptionContractCalculation
  API.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/edit-contracts-with-bundles
  md: >-
    https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/edit-contracts-with-bundles.md
api_name: admin
---

# Edit subscription contracts with bundles

A subscription contract can contain [bundle](https://shopify.dev/docs/apps/build/product-merchandising/bundles) products. This guide explains how a bundle is represented on a contract, how pricing is allocated between a bundle parent line and its component lines, and how to add, update, and remove bundles when you [update a subscription contract](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/update-a-subscription-contract) with the `SubscriptionContractCalculation` API.

**Available in the 2026-10 release candidate:**

Bundle support in contract edits is available with the `SubscriptionContractCalculation` API in the GraphQL Admin API `2026-10` release candidate. The legacy `SubscriptionDraft` API doesn't support bundles in contract edits.

***

## Requirements

**Note:**

* Most subscriptions, pre-order and try before you buy apps need to request API access through the [Partner Dashboard](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/authorization-code-grant#ask-for-permission). We give API access to apps that are designed according to our [principles for subscriptions, pre-order and TBYB apps](https://shopify.dev/docs/apps/selling-strategies/purchase-options#shopifys-principles).
* Public apps that use subscriptions, pre-order or TBYB need to meet [specific requirements](https://shopify.dev/docs/apps/launch/app-requirements-checklist#purchase-option-apps) to be published on the Shopify App Store.
* Custom apps [created in the Shopify admin](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/generate-app-access-tokens-admin) can't use subscriptions, pre-order or TBYB because these apps can't use extensions or request access to protected scopes. If you're building a solution for a single store, then build your custom app in the Partner Dashboard.

- Your app can make [authenticated requests](https://shopify.dev/docs/api/admin-graphql#authentication) to the GraphQL Admin API.
- Your app has the `read_own_subscription_contracts` and `write_own_subscription_contracts` [access scopes](https://shopify.dev/docs/api/usage/access-scopes). Learn how to [configure your access scopes using Shopify CLI](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration).
- You've created [products](https://shopify.dev/docs/api/admin-graphql/latest/mutations/productcreate) and [product variants](https://shopify.dev/docs/api/admin-graphql/latest/mutations/productvariantcreate) in your development store.

* Use the GraphQL Admin API version `2026-10` or later.
* Familiarize yourself with [updating a subscription contract](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/update-a-subscription-contract), including the calculate, poll, and commit flow.

***

## How bundles appear on a contract

A contract exposes its lines in two ways:

* [`lines`](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/SubscriptionContract#field-SubscriptionContract.fields.lines): A flat connection. Every sellable line has an entry, including each component of a bundle as its own [`SubscriptionLine`](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/SubscriptionLine).
* [`groupedLines`](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/SubscriptionContract#field-SubscriptionContract.fields.groupedLines): A grouped connection of [`SubscriptionGroupedLine`](https://shopify.dev/docs/api/admin-graphql/2026-10/unions/SubscriptionGroupedLine) values. A standalone line is returned as a `SubscriptionLine`. A bundle is returned as a [`SubscriptionParentLine`](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/SubscriptionParentLine) with its component lines nested under `components`.

A component's membership in a bundle is expressed structurally by its position under a parent node in `groupedLines`. Component lines don't carry a flat back-reference to their parent, so use `groupedLines` whenever you need the bundle structure.

The `SubscriptionParentLine` type describes the bundle parent:

| Field | Description |
| - | - |
| `id` | The unique ID of the parent line. |
| `components` | The component `SubscriptionLine` values that belong to the bundle. |
| `productId`, `variantId` | The bundle product and product variant, when the bundle is variant-backed. |
| `quantity` | The quantity of the bundle. |
| `title`, `presentmentTitle` | The product title and the customer-facing title of the bundle. |
| `sourceType` | How the bundle was composed: `BUNDLES_STATIC_EXPAND` for a fixed bundle expanded from its stored definition, `BUNDLES_DYNAMIC_EXPAND` for a bundle parent variant expanded by a [cart transform](https://shopify.dev/docs/api/functions/latest/cart-transform) function, `BUNDLES_DYNAMIC_MERGE` for separate lines merged into a bundle by a cart transform function, and `BUNDLES_SOURCE_PROVIDED` for a [custom bundle](#terminology). |
| `bundledBy` | The app that bundled the parent line. |
| `customAttributes` | Custom attributes on the parent line. |

The following query returns both views of the contract's lines:

## POST https://{shop}.myshopify.com/admin/api/2026-10/graphql.json

## GraphQL query

```graphql
query GetContractWithBundles($id: ID!) {
  subscriptionContract(id: $id) {
    id
    lines(first: 50) {
      nodes {
        id
        variantId
        title
        quantity
        currentPrice {
          amount
          currencyCode
        }
      }
    }
    groupedLines(first: 50) {
      nodes {
        ... on SubscriptionLine {
          id
          variantId
          title
          quantity
        }
        ... on SubscriptionParentLine {
          id
          variantId
          title
          quantity
          sourceType
          components {
            id
            variantId
            title
            quantity
            currentPrice {
              amount
              currencyCode
            }
          }
        }
      }
    }
  }
}
```

## Variables

```json
{
  "id": "gid://shopify/SubscriptionContract/123"
}
```

**Partial bundle subscriptions:**

When a customer subscribes to only some components of a bundle, the subscribed components are stored as standalone lines without a parent line. Edit them like any other product variant line. Only fully subscribed bundles appear as a `SubscriptionParentLine` in `groupedLines`.

***

## Pricing

Pricing for a bundle lives on its component lines:

* Each component line carries its own unit price (`currentPrice`) and discounted total (`lineDiscountedPrice`).
* The bundle parent line doesn't have an independent price. Its value is the per-bundle-unit sum of its component prices.
* When you supply a [custom bundle](#terminology), each component's `priceOverride` sets that component's unit price. Components without an override use the price that Shopify calculates for them.
* The parent line's `quantity` multiplies each component's quantity. A bundle with quantity 3 that contains a component with quantity 2 results in a component line with quantity 6. Component unit prices aren't multiplied.

Discounts apply to component lines the same way they apply to standalone lines. For how to apply discount codes, order and delivery discounts, and line-scoped discounts during a calculation, refer to [Manage discounts](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/migrate-to-subscription-calculation-api#manage-discounts).

***

## Editing behavior

Bundle handling during a contract edit is controlled by the required `withMerchandiseCustomizations` field on the calculate input, which determines whether merchandise transformations run during the calculation: both fixed bundle expansion from a stored definition and Shopify Functions that customize merchandise, such as cart transforms. The following table summarizes how each line input behaves when customizations are enabled (`true`) or disabled (`false`):

| Line input | Customizations enabled | Customizations disabled |
| - | - | - |
| `productVariantLine` whose variant is a bundle parent ([`requiresComponents`](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/ProductVariant#field-ProductVariant.fields.requiresComponents): `true`) | Expanded into its component lines during calculation. | Rejected. Variants that require components can't be supplied as standalone lines. |
| `productVariantLine` values that a cart transform function merges | Merged into a single bundle during calculation. | Accepted as standalone lines. No merge occurs. |
| `productVariantParentLine` (a [custom bundle](#terminology)) | Rejected. Custom bundle compositions are never rebuilt. | Accepted. Lines are taken exactly as provided. |
| `lines` omitted (contract already contains a bundle) | Stored [expanded and merged bundles](#terminology) are [rebuilt](#terminology) from their current definitions. Rejected if the contract contains a custom bundle. | The stored bundle structure is preserved unchanged. |

As with any contract update, the `lines` array replaces the complete collection. Include every line that you want to keep, and omit the lines that compose a bundle to remove the bundle and all of its components. See [Update or remove a bundle](#update-or-remove-a-bundle) to learn more.

### Add an expanded or merged bundle

Shopify composes bundles during calculation in the following ways:

* **Expand**: A bundle parent variant is expanded into its component lines, from a fixed bundle definition (`BUNDLES_STATIC_EXPAND`) or a cart transform function's expand operation (`BUNDLES_DYNAMIC_EXPAND`).
* **Merge**: Separate lines are merged into a single bundle by a cart transform function's merge operation (`BUNDLES_DYNAMIC_MERGE`).

To add a bundle that expands, supply the bundle's parent product variant as a regular `productVariantLine` and set `withMerchandiseCustomizations: true`. The calculation expands the parent into its component lines and applies the bundle's pricing:

## POST https://{shop}.myshopify.com/admin/api/2026-10/graphql.json

## GraphQL mutation

```graphql
mutation AddBundleToContract {
  subscriptionContractUpdateCalculate(
    contractId: "gid://shopify/SubscriptionContract/123"
    contractUpdateInput: {
      withMerchandiseCustomizations: true
      lines: [
        {
          productVariantLine: {
            # Keep an existing line by including its ID
            id: "gid://shopify/SubscriptionLine/existing-line-uuid"
            productVariantId: "gid://shopify/ProductVariant/111"
            quantity: 1
            customAttributes: []
            discounts: []
          }
        }
        {
          productVariantLine: {
            # Add a bundle: omit the ID and use the bundle parent variant
            productVariantId: "gid://shopify/ProductVariant/222"
            quantity: 1
            customAttributes: []
            discounts: []
          }
        }
      ]
    }
  ) {
    subscriptionContractCalculation {
      ... on SubscriptionContractCalculationPending {
        id
      }
    }
    userErrors {
      field
      message
      code
    }
  }
}
```

## JSON response

```json
{
  "data": {
    "subscriptionContractUpdateCalculate": {
      "subscriptionContractCalculation": {
        "id": "gid://shopify/SubscriptionContractCalculation/789"
      },
      "userErrors": []
    }
  }
}
```

If you supply a bundle parent variant this way with `withMerchandiseCustomizations: false`, then the mutation returns a user error: `Product variants that require components can't be supplied as standalone product variant lines. Provide components or set withMerchandiseCustomizations: true.`

To add a bundle that merges, supply each line that the shop's cart transform function merges as its own `productVariantLine` and set `withMerchandiseCustomizations: true`. During calculation, the function merges the matching lines into a bundle parent line with its components nested under it. Unlike a bundle parent variant, these lines are ordinary sellable variants: if you supply them with `withMerchandiseCustomizations: false`, then they're accepted but remain standalone lines, and no bundle is composed.

### Add a custom bundle

To control a bundle's composition yourself, add a [custom bundle](#terminology): supply a `productVariantParentLine` with the parent variant and an explicit list of components, and set `withMerchandiseCustomizations: false`. Shopify stores the composition exactly as provided and never rebuilds it from the bundle's definition:

## POST https://{shop}.myshopify.com/admin/api/2026-10/graphql.json

## GraphQL mutation

```graphql
mutation UpdateContractWithExplicitBundle {
  subscriptionContractUpdateCalculate(
    contractId: "gid://shopify/SubscriptionContract/123"
    contractUpdateInput: {
      withMerchandiseCustomizations: false
      lines: [
        {
          productVariantParentLine: {
            productVariantId: "gid://shopify/ProductVariant/222"
            quantity: 1
            customAttributes: []
            components: [
              {
                productVariantLine: {
                  productVariantId: "gid://shopify/ProductVariant/333"
                  quantity: 2
                  priceOverride: { amount: "7.00", currencyCode: CAD }
                  customAttributes: []
                  discounts: []
                }
              }
              {
                productVariantLine: {
                  productVariantId: "gid://shopify/ProductVariant/444"
                  quantity: 1
                  priceOverride: { amount: "3.00", currencyCode: CAD }
                  customAttributes: []
                  discounts: []
                }
              }
            ]
          }
        }
      ]
    }
  ) {
    subscriptionContractCalculation {
      ... on SubscriptionContractCalculationPending {
        id
      }
    }
    userErrors {
      field
      message
      code
    }
  }
}
```

## JSON response

```json
{
  "data": {
    "subscriptionContractUpdateCalculate": {
      "subscriptionContractCalculation": {
        "id": "gid://shopify/SubscriptionContractCalculation/789"
      },
      "userErrors": []
    }
  }
}
```

The `productVariantParentLine` input accepts the following fields:

| Field | Required | Description |
| - | - | - |
| `id` | No | The ID of an existing parent line to update. Omit for new parent lines. |
| `productVariantId` | Yes | The product variant for the bundle parent line. |
| `quantity` | Yes | The quantity of the bundle. Must be at least 1. |
| `components` | Yes | The component lines for the bundle. Each component is a `productVariantLine` with the same fields as a standalone line, including `priceOverride` and `discounts`. Component quantities are per bundle unit, and are multiplied by the parent line's `quantity` during calculation. |
| `customAttributes` | Yes (default `[]`) | Custom attributes for the parent line. |

If you supply a `productVariantParentLine` with `withMerchandiseCustomizations: true`, then the mutation returns a user error: `Source-provided bundles are only supported with withMerchandiseCustomizations: false.` API messages and the `sourceType` value refer to custom bundles as source-provided.

### Update or remove a bundle

Because `lines` replaces the complete collection, editing a bundle means restating it:

* **Update a custom bundle**: Include the parent line's `id` and the complete desired `components` list. The supplied components replace the bundle's existing components, and their quantities are per bundle unit.
* **Update an expanded or merged bundle**: With `withMerchandiseCustomizations: true`, restate the lines that compose the bundle along with your other changes: the bundle's parent variant line for a bundle that expands, or the individual lines that the function merges for a bundle that merges. The calculation [rebuilds](#terminology) the bundle from its current definition.
* **Remove a bundle**: Omit the lines that compose the bundle from `lines`: the bundle's parent line for a bundle that expands, or the lines that the function merges for a bundle that merges. The parent and all of its component lines are removed. Components can't be removed individually from an expanded or merged bundle, because the composition comes from the bundle's definition.

### Keep existing bundles unchanged

To edit other parts of a contract without recalculating its bundles, omit the `lines` field and set `withMerchandiseCustomizations: false`. The stored bundle structure, including component quantities and unit prices, is preserved exactly.

Omitting `lines` isn't sufficient on its own: when `lines` is omitted with `withMerchandiseCustomizations: true`, stored expanded and merged bundles are [rebuilt](#terminology) from their current definitions instead. If the contract contains a [custom bundle](#terminology), this returns a user error: `The contract has a custom bundle that cannot be re-derived. Set withMerchandiseCustomizations: false to preserve the stored bundle.`

### Review the calculated bundle structure

After calculating, poll the calculation and query `groupedLines` on the calculated contract to verify the bundle structure before committing:

## POST https://{shop}.myshopify.com/admin/api/2026-10/graphql.json

## GraphQL query

```graphql
query GetCalculatedBundles($id: ID!) {
  subscriptionContractCalculation(id: $id) {
    ... on SubscriptionContractCalculationSuccess {
      id
      calculatedContract {
        groupedLines(first: 50) {
          nodes {
            ... on SubscriptionLine {
              id
              variantId
              title
              quantity
            }
            ... on SubscriptionParentLine {
              id
              variantId
              title
              quantity
              sourceType
              components {
                id
                variantId
                title
                quantity
                currentPrice {
                  amount
                  currencyCode
                }
                lineDiscountedPrice {
                  amount
                  currencyCode
                }
              }
            }
          }
        }
      }
    }
    ... on SubscriptionContractCalculationFailure {
      errors {
        code
        field
        message
      }
    }
  }
}
```

## Variables

```json
{
  "id": "gid://shopify/SubscriptionContractCalculation/789"
}
```

Then commit the calculation as described in [Update a subscription contract](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/update-a-subscription-contract#step-4-commit-the-calculation).

***

## Considerations

* **Lines replacement**: The `lines` array replaces all existing lines. Omitting the lines that compose a bundle removes the bundle and its components. For a bundle that expands, omit its parent line. For a bundle that merges, omit the lines that the cart transform function merges.
* **Custom bundles are never rebuilt**: A [custom bundle](#terminology) always calculates with `withMerchandiseCustomizations: false`, both when you restate it and when you omit `lines` on a later edit.
* **Deleted components**: If a component variant was deleted from the catalog, then an edit with `withMerchandiseCustomizations: false` preserves the stored component as a `customLine` without a variant, while an edit with `withMerchandiseCustomizations: true` [rebuilds](#terminology) the bundle from its current definition, which drops the deleted component.
* **Deleted bundle parent variants**: If a stored bundle's parent variant no longer exists, then a calculation with `withMerchandiseCustomizations: true` fails with an error that suggests supplying replacement lines or setting `withMerchandiseCustomizations: false` to preserve the stored bundle.
* **Partial bundle subscriptions**: Contracts created from a selling plan that covers only some bundle components store those components as standalone lines. They have no parent line, and you edit them as regular product variant lines.

***

## Terminology

| Term | Meaning |
| - | - |
| Expanded bundle | A bundle whose composition Shopify resolves from its definition: either a fixed bundle definition (`BUNDLES_STATIC_EXPAND`), or a cart transform function that expands a parent variant (`BUNDLES_DYNAMIC_EXPAND`). Expanded bundles calculate with `withMerchandiseCustomizations: true`. |
| Merged bundle | A bundle whose composition Shopify resolves from its definition: a cart transform function that merges separate lines into a single bundle (`BUNDLES_DYNAMIC_MERGE`). Unlike an expanded bundle, a merged bundle is always dynamic and there's no fixed-definition equivalent. Merged bundles calculate with `withMerchandiseCustomizations: true`. |
| Custom bundle | A bundle whose composition the app supplies directly in the contract input as a `productVariantParentLine` with explicit components. The composition doesn't need to exist as a bundle definition in the shop, and Shopify never rebuilds it. Custom bundles calculate with `withMerchandiseCustomizations: false`. API error messages and the `sourceType` value refer to custom bundles as source-provided (`BUNDLES_SOURCE_PROVIDED`). |
| Rebuild | Recompute a bundle's composition from its current definition during a calculation. If the definition changed after the contract was created, then the rebuilt bundle reflects the change: components can be added, removed, or swapped. For example, if a bundle's definition changed from a coffee machine and a coffee pod to a coffee machine and a tea pod, then rebuilding replaces the coffee pod component with a tea pod component. For a bundle composed by a cart transform function, rebuilding runs the function again, so the function's current output determines the bundle's composition. |

***

## Next steps

* Learn how to [update a subscription contract](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/update-a-subscription-contract), including the full calculate, poll, and commit flow.
* Refer to the [`SubscriptionContractCalculationLineInput`](https://shopify.dev/docs/api/admin-graphql/2026-10/input-objects/SubscriptionContractCalculationLineInput) and [`SubscriptionParentLine`](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/SubscriptionParentLine) reference documentation.

***
