Edit subscription contracts with bundles
A subscription contract can contain bundle 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 with the SubscriptionContractCalculation API.
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.
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.
Anchor to RequirementsRequirements
- Most subscriptions, pre-order and try before you buy apps need to request API access through the Partner Dashboard. We give API access to apps that are designed according to our principles for subscriptions, pre-order and TBYB apps.
- Public apps that use subscriptions, pre-order or TBYB need to meet specific requirements to be published on the Shopify App Store.
- Custom apps created in the Shopify 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.
- Most subscriptions, pre-order and try before you buy apps need to request API access through the Partner Dashboard. We give API access to apps that are designed according to our principles for subscriptions, pre-order and TBYB apps.
- Public apps that use subscriptions, pre-order or TBYB need to meet specific requirements to be published on the Shopify App Store.
- Custom apps created in the Shopify 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 to the GraphQL Admin API.
- Your app has the
read_own_subscription_contractsandwrite_own_subscription_contractsaccess scopes. Learn how to configure your access scopes using Shopify CLI. - You've created products and product variants in your development store.
- Use the GraphQL Admin API version
2026-10or later. - Familiarize yourself with updating a subscription contract, including the calculate, poll, and commit flow.
Anchor to How bundles appear on a contractHow bundles appear on a contract
A contract exposes its lines in two ways:
lines: A flat connection. Every sellable line has an entry, including each component of a bundle as its ownSubscriptionLine.groupedLines: A grouped connection ofSubscriptionGroupedLinevalues. A standalone line is returned as aSubscriptionLine. A bundle is returned as aSubscriptionParentLinewith its component lines nested undercomponents.
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 function, BUNDLES_DYNAMIC_MERGE for separate lines merged into a bundle by a cart transform function, and BUNDLES_SOURCE_PROVIDED for a custom bundle. |
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
Variables
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.
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.
Anchor to PricingPricing
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, each component's
priceOverridesets that component's unit price. Components without an override use the price that Shopify calculates for them. - The parent line's
quantitymultiplies 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.
Anchor to Editing behaviorEditing 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: 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) | 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 are rebuilt 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 to learn more.
Anchor to Add an expanded or merged bundleAdd 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
JSON response
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.
Anchor to Add a custom bundleAdd a custom bundle
To control a bundle's composition yourself, add a custom bundle: 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
JSON response
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.
Anchor to Update or remove a bundleUpdate 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
idand the complete desiredcomponentslist. 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 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.
Anchor to Keep existing bundles unchangedKeep 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 from their current definitions instead. If the contract contains a custom bundle, this returns a user error: The contract has a custom bundle that cannot be re-derived. Set withMerchandiseCustomizations: false to preserve the stored bundle.
Anchor to Review the calculated bundle structureReview 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
Variables
Then commit the calculation as described in Update a subscription contract.
Anchor to ConsiderationsConsiderations
- Lines replacement: The
linesarray 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 always calculates with
withMerchandiseCustomizations: false, both when you restate it and when you omitlineson a later edit. - Deleted components: If a component variant was deleted from the catalog, then an edit with
withMerchandiseCustomizations: falsepreserves the stored component as acustomLinewithout a variant, while an edit withwithMerchandiseCustomizations: truerebuilds 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: truefails with an error that suggests supplying replacement lines or settingwithMerchandiseCustomizations: falseto 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.
Anchor to TerminologyTerminology
| 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. |
Anchor to Next stepsNext steps
- Learn how to update a subscription contract, including the full calculate, poll, and commit flow.
- Refer to the
SubscriptionContractCalculationLineInputandSubscriptionParentLinereference documentation.