Skip to main content

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.

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.


Note
  • 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.
  • Use the GraphQL Admin API version 2026-10 or 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:

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:

FieldDescription
idThe unique ID of the parent line.
componentsThe component SubscriptionLine values that belong to the bundle.
productId, variantIdThe bundle product and product variant, when the bundle is variant-backed.
quantityThe quantity of the bundle.
title, presentmentTitleThe product title and the customer-facing title of the bundle.
sourceTypeHow 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.
bundledByThe app that bundled the parent line.
customAttributesCustom 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

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

{
"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 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 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.


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 inputCustomizations enabledCustomizations 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 mergesMerged 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

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

{
"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.

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

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

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

The productVariantParentLine input accepts the following fields:

FieldRequiredDescription
idNoThe ID of an existing parent line to update. Omit for new parent lines.
productVariantIdYesThe product variant for the bundle parent line.
quantityYesThe quantity of the bundle. Must be at least 1.
componentsYesThe 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.
customAttributesYes (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 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 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

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

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

Then commit the calculation as described in Update a subscription contract.


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

TermMeaning
Expanded bundleA 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 bundleA 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 bundleA 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).
RebuildRecompute 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.


Was this page helpful?