---
title: Combine subscription contracts
description: >-
  Learn how to combine the content of multiple subscription contracts together
  to save on shipping and administrative costs.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/combine-subscription-contracts
  md: >-
    https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/combine-subscription-contracts.md
api_name: admin
---

# Combine subscription contracts

When a customer has several subscription contracts whose billing cycles fall on the same date, you can concatenate those cycles so that they bill as one order. One billing attempt on any cycle in the group creates one order that's linked to every contract in the group. The recurring contracts don't change. For the complete input model and reference tables, refer to [Concatenate billing cycles](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/migrate-to-subscription-calculation-api#concatenate-billing-cycles) in the migration guide.

**Early access:**

`subscriptionBillingCycleContractConcatenateCalculate` is available on the `unstable` API version for apps that Shopify has enabled. To request access or send feedback, email <subscriptions-calculate-api-early-access@shopify.com>. If your app uses the `SubscriptionDraft` API, refer to the [legacy `SubscriptionDraft` combine guide](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/combine-subscription-contracts-with-subscription-draft).

***

## Requirements

**Note:**

* Most subscriptions, pre-order and try before you buy apps need to request API access through the [Partner Dashboard](https://partners.shopify.com/current/apps). 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/build/purchase-options#shopifys-principles).
* Public apps that use subscriptions, pre-order or TBYB need to meet [specific requirements](https://shopify.dev/docs/apps/launch/shopify-app-store/app-store-requirements#purchase-option) to be published on the Shopify App Store.
* Custom apps [created in the Shopify admin](https://shopify.dev/docs/apps/build/authentication-authorization/legacy/admin-custom-apps) 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 [create your custom app in the Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard/create-apps-using-dev-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#authenticated-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/productVariantsBulkCreate) in your development store.

* Ask Shopify to enable your app for the early access `subscriptionBillingCycleContractConcatenateCalculate` mutation.
* Use the `unstable` GraphQL Admin API version.
* Create at least two active [subscription contracts](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/build-a-subscription-contract) for the same customer. Each contract must have recurring billing and delivery policies.
* Familiarize yourself with [billing cycles](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/billing-cycles) and [managing billing cycle contracts](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/billing-cycles/manage-billing-cycle-contracts).

***

## What you can create

The following diagram shows a preview of how combining contracts works.

![The combined contracts process workflow, as described below.](https://shopify.dev/assets/assets/images/apps/subscriptions/combined-contracts-BvqsVum9.png)

In this example, the customer has two contracts, one weekly and one bi-weekly, for coffee and tea. Both contracts have billing cycles on `01/01`, `01/15`, and `01/29`. You can combine the subscription orders to consolidate fulfillment and shipping costs into one order.

***

## How concatenation works

The contract that you pass as `contractId` is the anchor. The order bills on the anchor's cycle and uses its payment method and policies. The cycles in `concatenatedBillingCycles` are members. Committing attaches each member cycle to the anchor's edited contract, so one billing attempt bills the whole group. The recurring contracts don't change.

The calculate API doesn't merge content from the contracts. The `lines`, `manualDiscounts`, and `discountCodes` fields describe the complete order. A line that you leave out isn't billed. You must also provide the delivery method for the combined order.

Concatenation uses an asynchronous flow that's scoped to the selected cycles:

1. Query each cycle and its current contract state.
2. Submit the anchor, the members, and the complete order.
3. Poll the calculation or wait for a webhook.
4. Review the calculated result.
5. Commit a successful calculation.

The cycles must meet the following requirements:

* No cycle in the group has ended, is skipped, has a billing attempt that hasn't failed, or is already part of a concatenation.
* Every contract in the group is active and isn't prepaid.
* Every member contract belongs to the same customer, uses the same currency, and is owned by the same app as the anchor.
* Include between one and 50 member cycles. Exclude the anchor, and name each contract only once.

Refer to the migration guide for the complete [requirements on the cycles](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/migrate-to-subscription-calculation-api#requirements-on-the-cycles).

***

## Step 1: Read the lines on each cycle

Query each participating billing cycle to collect the line IDs and values that you'll restate in the combined order. If `editedContract` is set, then read its lines. If `editedContract` is `null`, then read the lines from `sourceContract`. Don't use `edited` to select the contract, because a schedule-only edit also sets `edited` to `true`. The `variantId` value is the `productVariantId` value that you pass in [Step 2](#step-2-calculate-the-concatenation).

## POST https://{shop}.myshopify.com/admin/api/{api\_version}/graphql.json

## GraphQL query

```graphql
query GetBillingCycleLines {
  subscriptionBillingCycle(
    billingCycleInput: {
      contractId: "gid://shopify/SubscriptionContract/123"
      selector: { index: 2 }
    }
  ) {
    cycleIndex
    edited
    sourceContract {
      lines(first: 50) {
        nodes {
          ...SubscriptionLineFields
        }
      }
    }
    editedContract {
      lines(first: 50) {
        nodes {
          ...SubscriptionLineFields
        }
      }
    }
  }
}


fragment SubscriptionLineFields on SubscriptionLine {
  id
  quantity
  variantId
  currentPrice {
    amount
    currencyCode
  }
  customAttributes {
    key
    value
  }
  sellingPlanId
  sellingPlanName
  pricingPolicy {
    basePrice {
      amount
      currencyCode
    }
    cycleDiscounts {
      afterCycle
      adjustmentType
      adjustmentValue {
        ... on MoneyV2 {
          amount
          currencyCode
        }
        ... on SellingPlanPricingPolicyPercentageValue {
          percentage
        }
      }
      computedPrice {
        amount
        currencyCode
      }
    }
  }
}
```

Anchor lines keep their `id`. Member lines use their ID as `originLineId` in Step 2. Each line input in Step 2 is a complete restatement of the line, so read every value that you want to keep. A field that you leave out is cleared or recalculated, even when you include `id` or `originLineId`. If a line has line-scoped discounts, read them too and restate them in `discounts`. Refer to the migration guide's [line fields](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/migrate-to-subscription-calculation-api#line-fields) for the mapping from `SubscriptionLine` to the line input, and to [Preserve a line's state when you change it](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/migrate-to-subscription-calculation-api#preserve-a-lines-state-when-you-change-it) for the rule.

***

## Step 2: Calculate the concatenation

Call `subscriptionBillingCycleContractConcatenateCalculate` with the anchor, the member cycles, and the complete order. Map `currentPrice` to `priceOverride`, `sellingPlanId` to `originSellingPlanId`, and `pricingPolicy` to `appManagedPricingPolicy`. `discounts` is required; pass `[]` when the line has no line-scoped discounts. The following example restates the values that Step 1 returned for two lines without a pricing policy:

## POST https://{shop}.myshopify.com/admin/api/{api\_version}/graphql.json

## Create concatenation calculation

```graphql
mutation ConcatenateCalculation {
  subscriptionBillingCycleContractConcatenateCalculate(
    # The anchor: the order bills on this contract's cycle, with its payment method and policies
    contractId: "gid://shopify/SubscriptionContract/123"
    billingCycleSelector: { index: 2 }
    billingCycleConcatenateInput: {
      # Control contract calculation behavior
      withMerchandiseCustomizations: true


      # The members: the cycles this order satisfies. At least one is required.
      concatenatedBillingCycles: [
        {
          contractId: "gid://shopify/SubscriptionContract/456"
          selector: { index: 2 }
        }
      ]


      # The complete set of lines for the combined order. Nothing is inherited.
      lines: [
        {
          productVariantLine: {
            # A line of the anchor contract: include its id
            id: "gid://shopify/SubscriptionLine/anchor-line-uuid"
            productVariantId: "gid://shopify/ProductVariant/111"
            quantity: 1
            priceOverride: { amount: "20.00", currencyCode: CAD }
            customAttributes: [{ key: "roast", value: "medium" }]
            originSellingPlanId: "gid://shopify/SellingPlan/1001"
            sellingPlanName: "Coffee every two weeks"
            discounts: []
          }
        }
        {
          productVariantLine: {
            # A line of a member contract: name it with originLineId, not id
            originLineId: "gid://shopify/SubscriptionLine/member-line-uuid"
            productVariantId: "gid://shopify/ProductVariant/222"
            quantity: 2
            priceOverride: { amount: "15.00", currencyCode: CAD }
            customAttributes: [{ key: "size", value: "500 g" }]
            originSellingPlanId: "gid://shopify/SellingPlan/1002"
            sellingPlanName: "Tea every two weeks"
            discounts: []
          }
        }
      ]


      # The delivery method for the combined order. Required, not inherited.
      deliveryMethod: {
        shipping: {
          address: {
            firstName: "Quinn"
            lastName: "Ishida"
            address1: "123 Main St"
            city: "Toronto"
            provinceCode: "ON"
            countryCode: CA
            zip: "M5V 1A1"
          }
          title: "Standard"
          code: "Standard"
          deliveryPrice: {
            amount: "5.00"
            currencyCode: CAD
          }
        }
      }


      # Discounts for the combined order (pass empty arrays for none)
      discountCodes: []
      manualDiscounts: []


      # Custom attributes for the combined order (pass an empty array for none)
      customAttributes: []
    }
  ) {
    subscriptionContractCalculation {
      ... on SubscriptionContractCalculationPending {
        id
      }
    }
    userErrors {
      field
      message
      code
    }
  }
}
```

## JSON response

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

The `lines` array is the complete order. Use `id` for an anchor line and `originLineId` for a member line. `originLineId` must identify a line on a member contract and can't be combined with `id`. Omit both fields to add a new line.

**Members without lines are still billed:**

Every contract you name in `concatenatedBillingCycles` is billed by the combined order, even when none of your `lines` carry an `originLineId` from it. Its cycle is marked as billed and points to an order that contains none of its products. Include a line for every member unless you intend this.

***

## Step 3: Poll for the calculation result

The calculate mutation returns a pending calculation. Poll for the result and review it before committing. The `SubscriptionContractCalculationSuccess` result contains `projectedOrderTotals` for the combined order.

Contract calculations run asynchronously. Most calculations finish in less than three seconds, but Functions and external services can increase processing time. Use the calculation ID returned by the calculate mutation to query [`subscriptionContractCalculation`](https://shopify.dev/docs/api/admin-graphql/2026-10/queries/subscriptionContractCalculation):

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

## GraphQL query

```graphql
query PollSubscriptionContractCalculation($id: ID!) {
  subscriptionContractCalculation(id: $id) {
    __typename
    ... on SubscriptionContractCalculationPending {
      id
    }
    ... on SubscriptionContractCalculationSuccess {
      id
      calculatedContract {
        lines(first: 10) {
          nodes {
            id
            quantity
            title
            variantId
          }
        }
      }
      warnings {
        code
        message
      }
      projectedOrderTotals {
        subtotal {
          amount
          currencyCode
        }
        totalDelivery {
          amount
          currencyCode
        }
        totalTax {
          amount
          currencyCode
        }
        totalMerchandiseDiscounts {
          amount
          currencyCode
        }
        totalDeliveryDiscounts {
          amount
          currencyCode
        }
        total {
          amount
          currencyCode
        }
      }
    }
    ... on SubscriptionContractCalculationFailure {
      id
      errors {
        code
        message
      }
    }
  }
}
```

## Variables

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

## Pending response

```json
{
  "data": {
    "subscriptionContractCalculation": {
      "__typename": "SubscriptionContractCalculationPending",
      "id": "gid://shopify/SubscriptionContractCalculation/789"
    }
  }
}
```

The query returns one of the following types:

| Type | Meaning | Action |
| - | - | - |
| `SubscriptionContractCalculationPending` | Shopify initiated or is processing the calculation. | Continue polling. |
| `SubscriptionContractCalculationSuccess` | The calculated contract is ready to review and commit. | Review the result before committing. |
| `SubscriptionContractCalculationFailure` | The calculation failed (processed with errors) or Shopify voided it (never processed, for example, because of an infrastructure issue). | If it failed, read `errors` and correct the input before calculating again. If it was voided, `errors` is empty and there's nothing to fix in the input, so retry the calculation. |

Wait two seconds before the first poll, poll every second, and stop after 30 seconds. If the calculation is still pending, then retry later or wait for a webhook instead of increasing the polling frequency.

### Review the calculated contract

Before committing, inspect the following fields on `SubscriptionContractCalculationSuccess`:

* `calculatedContract`: The complete contract snapshot that the commit applies.
* `projectedOrderTotals`: The projected merchandise, delivery, discount, tax, and total amounts.
* `warnings`: Existing-data or compatibility problems that don't prevent the calculation from succeeding.

A successful calculation is immutable. If you need to change the input, then start a new calculation and commit only the result that you want to make active.

**Review warnings:**

A successful calculation can contain warnings. Review them before committing because they can identify disabled currencies, delivery configuration problems, or other existing contract data that Shopify preserved instead of blocking the update.

### Use webhooks instead of polling

For event-driven processing, subscribe to the following webhook topics:

| Topic | Description |
| - | - |
| `subscription_contract_calculations/succeed` | The calculation succeeded and is ready to review and commit. |
| `subscription_contract_calculations/fail` | The calculation failed or Shopify voided it. A failed calculation has `errors` to query; a voided one has empty `errors`, so retry it. |

Create a webhook subscription with [`webhookSubscriptionCreate`](https://shopify.dev/docs/api/admin-graphql/2026-10/mutations/webhookSubscriptionCreate):

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

## GraphQL mutation

```graphql
mutation CreateCalculationWebhook {
  webhookSubscriptionCreate(
    topic: SUBSCRIPTION_CONTRACT_CALCULATIONS_SUCCEED
    webhookSubscription: {
      uri: "https://example.com/webhooks/calculation-success"
      format: JSON
    }
  ) {
    webhookSubscription {
      id
    }
    userErrors {
      field
      message
    }
  }
}
```

## Webhook payload

```json
{
  "id": 789,
  "admin_graphql_api_id": "gid://shopify/SubscriptionContractCalculation/789",
  "state": "succeeded"
}
```

Use `admin_graphql_api_id` to query the completed calculation. Webhook delivery doesn't commit the result automatically.

***

## Step 4: Commit the calculation

Commit the successful calculation with `subscriptionContractCalculationCommit`. For a concatenation, the returned `SubscriptionBillingCycleEditedContract` lists every cycle in the group:

## POST https://{shop}.myshopify.com/admin/api/{api\_version}/graphql.json

## Commit concatenation calculation

```graphql
mutation CommitCalculation {
  subscriptionContractCalculationCommit(
    id: "gid://shopify/SubscriptionContractCalculation/789"
  ) {
    contract {
      ... on SubscriptionBillingCycleEditedContract {
        billingCycles(first: 10) {
          nodes {
            cycleIndex
            sourceContract { id }
          }
        }
      }
    }
    userErrors {
      field
      message
    }
  }
}
```

## JSON response

```json
{
  "data": {
    "subscriptionContractCalculationCommit": {
      "contract": {
        "billingCycles": {
          "nodes": [
            { "cycleIndex": 2, "sourceContract": { "id": "gid://shopify/SubscriptionContract/123" } },
            { "cycleIndex": 2, "sourceContract": { "id": "gid://shopify/SubscriptionContract/456" } }
          ]
        }
      },
      "userErrors": []
    }
  }
}
```

Shopify checks the [requirements on the cycles](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/migrate-to-subscription-calculation-api#requirements-on-the-cycles) again when you commit.

***

## Step 5: Fetch the combined contract

After the commit, query any cycle in the group to read the shared edited contract and confirm the group membership. Every member returns the same `editedContract` with `edited: true`. The `billingCycles` connection lists the whole group:

## POST https://{shop}.myshopify.com/admin/api/{api\_version}/graphql.json

## GraphQL query

```graphql
query GetCombinedContract {
  subscriptionBillingCycle(
    billingCycleInput: {
      contractId: "gid://shopify/SubscriptionContract/456"
      selector: { index: 2 }
    }
  ) {
    cycleIndex
    edited
    editedContract {
      lines(first: 50) {
        nodes {
          id
          quantity
          variantId
        }
      }
      billingCycles(first: 50) {
        nodes {
          cycleIndex
          sourceContract {
            id
          }
        }
      }
    }
  }
}
```

***

## Step 6: Create an order

When the billing date comes, call [`subscriptionBillingCycleCharge`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/subscriptionBillingCycleCharge) on any contract in the group to create one linked order:

## POST https://{shop}.myshopify.com/admin/api/{api\_version}/graphql.json

## GraphQL mutation

```graphql
mutation {
  subscriptionBillingCycleCharge(
    subscriptionContractId: "gid://shopify/SubscriptionContract/123"
    billingCycleSelector: { index: 2 }
  ) {
    subscriptionBillingAttempt {
      id
      state {
        __typename
        ... on SubscriptionBillingAttemptSuccessState {
          order {
            id
          }
        }
        ... on SubscriptionBillingAttemptFailedState {
          error {
            __typename
            ... on SubscriptionBillingAttemptPaymentError {
              code
            }
          }
        }
      }
    }
    userErrors {
      code
      message
    }
  }
}
```

A billing attempt on a concatenated cycle can return `BILLING_CYCLE_GROUP_ALREADY_ATTEMPTED`, `BILLING_CYCLE_GROUP_CYCLE_SKIPPED`, or `BILLING_CYCLE_GROUP_CONTENDED` in addition to the usual `BillingAttemptUserError` codes. Refer to [Bill the concatenated cycles](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/migrate-to-subscription-calculation-api#bill-the-concatenated-cycles) for details.

***

## Remove a concatenation

To dissolve a group before it bills, call [`subscriptionBillingCycleEditDelete`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/subscriptionBillingCycleEditDelete) on any cycle in the group. Shopify removes the shared edit from every cycle in the group, and each cycle returns to its own recurring contract:

## POST https://{shop}.myshopify.com/admin/api/{api\_version}/graphql.json

## Remove concatenation

```graphql
mutation RemoveConcatenation {
  subscriptionBillingCycleEditDelete(
    # Any contract in the group, with its concatenated cycle
    billingCycleInput: {
      contractId: "gid://shopify/SubscriptionContract/456"
      selector: { index: 2 }
    }
  ) {
    billingCycles {
      cycleIndex
      edited
      sourceContract { id }
    }
    userErrors {
      field
      message
      code
    }
  }
}
```

## JSON response

```json
{
  "data": {
    "subscriptionBillingCycleEditDelete": {
      "billingCycles": [
        { "cycleIndex": 2, "edited": false, "sourceContract": { "id": "gid://shopify/SubscriptionContract/123" } },
        { "cycleIndex": 2, "edited": false, "sourceContract": { "id": "gid://shopify/SubscriptionContract/456" } }
      ],
      "userErrors": []
    }
  }
}
```

**Schedule edits are removed too:**

`subscriptionBillingCycleEditDelete` removes both the contract edit and any schedule edit on every cycle in the group. A cycle whose billing date was rescheduled before it was concatenated goes back to its original schedule. Reapply the schedule edit with [`subscriptionBillingCycleScheduleEdit`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/subscriptionBillingCycleScheduleEdit) if you still need it.

***

## Next steps

* Learn how to [manage orders and fulfillments for prepaid subscriptions](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/fulfillments/sync-orders-subscriptions).
* Learn how to [bulk charge multiple contracts at the same time](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/billing-cycles/bulk-billing-cycles).
* Refer to the migration guide's [Concatenate billing cycles](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/migrate-to-subscription-calculation-api#concatenate-billing-cycles) section for the complete input reference.
* Use the [legacy `SubscriptionDraft` combine guide](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/contracts/combine-subscription-contracts-with-subscription-draft) while maintaining an older integration.

***
