Skip to main content

Migrate to the SubscriptionContractCalculation API

This guide walks you through migrating from the SubscriptionDraft API to the new SubscriptionContractCalculation API for managing subscription contracts.

Early access

This API is in early access. Send feedback to subscriptions-calculate-api-early-access@shopify.com.



Anchor to What's changing and whyWhat's changing and why

The new API integrates with Shopify's unified checkout and provides the following benefits:

  • Simplified API surface: Contract mutations are consolidated into four calculate operations plus a shared commit.
  • Consistent pricing: Tax and discount calculations align with checkout.
  • Function support: Cart transforms and delivery customizations. Other Shopify Functions will be supported in later releases.
  • Accurate cost breakdown: Totals for lines, delivery, taxes, duties, and discounts are available through the API.

The following table summarizes the main differences between SubscriptionDraft and the new SubscriptionContractCalculation API.

AspectSubscriptionDraftSubscriptionContractCalculation
ApproachMulti-step draft mutationsSingle contract calculation
ProcessingSynchronousAsynchronous (requires polling)
State managementServer maintains draft stateClient provides desired state. On update, omitted top-level fields are preserved, but a list you provide (such as lines) replaces the stored list.
WebhooksNoneWebhooks on success or failure

Anchor to Understand the new API modelUnderstand the new API model

Anchor to From drafts to contract calculationsFrom drafts to contract calculations

With the current SubscriptionDraft API, you create a draft, apply changes through multiple mutations, and then commit the draft. With the new SubscriptionContractCalculation API, you submit the desired state in a single request. The server calculates pricing, applies taxes, executes functions, and returns an immutable result snapshot. On contract updates, any top-level field you omit is left unchanged — you don't need to resend the entire contract. When you provide a list field such as lines or manualDiscounts, the list you send replaces the whole stored list, and each entry is a complete restatement rather than a patch. See Preserve a line's state when you change it.

Anchor to Asynchronous processingAsynchronous processing

Calculations run asynchronously. You can poll for results or subscribe to webhooks. Here's the flow:

  1. Call the calculate mutation.
  2. Receive a SubscriptionContractCalculationPending response.
  3. Poll the subscriptionContractCalculation query, or wait for a webhook.
  4. Receive a SubscriptionContractCalculationSuccess or SubscriptionContractCalculationFailure response when complete.
  5. Call subscriptionContractCalculationCommit to apply the changes.

The following Boolean fields control contract calculation behavior:

FieldLocationDescription
withMerchandiseCustomizationsTop-level create or update inputWhen set to true, enables cart transforms and other function-based merchandise modifications.
withDeliveryCustomizationsdeliveryMethod.fetchAvailableDeliveryOptionsWhen true (the default), runs delivery customization functions while fetching delivery options, so the returned options reflect the merchant's configured customizations. Set to false to bypass these functions and return the raw options.

Anchor to API migration referenceAPI migration reference

Old mutationNew approach
subscriptionContractCreateUse subscriptionContractCreateCalculate
subscriptionContractUpdateUse subscriptionContractUpdateCalculate
subscriptionContractAtomicCreateUse subscriptionContractCreateCalculate
subscriptionContractProductChangeUse subscriptionContractUpdateCalculate
subscriptionDraftLineAddInclude in lines[] input
subscriptionDraftLineUpdateInclude updated line with the same id in lines[]
subscriptionDraftLineRemoveOmit line from lines[]
subscriptionDraftDiscountAddInclude orderDiscount in manualDiscounts[] input
subscriptionDraftDiscountUpdateInclude updated orderDiscount with the same id in manualDiscounts[]
subscriptionDraftDiscountRemoveOmit discount from manualDiscounts[]
subscriptionDraftDiscountCodeApplyInclude in discountCodes[] input
subscriptionDraftFreeShippingDiscountAddInclude deliveryDiscount in manualDiscounts[]
subscriptionDraftFreeShippingDiscountUpdateInclude updated deliveryDiscount with the same id in manualDiscounts[]
subscriptionDraftUpdateRecalculate with additional input
subscriptionDraftCommitUse subscriptionContractCalculationCommit
subscriptionBillingCycleContractEditUse subscriptionBillingCycleContractEditCalculate
subscriptionBillingCycleContractDraftConcatenateUse subscriptionBillingCycleContractConcatenateCalculate (early access). It concatenates billing cycles only. To combine contracts permanently, use subscriptionContractUpdateCalculate. See Concatenate billing cycles.
subscriptionBillingCycleContractDraftCommitUse subscriptionContractCalculationCommit
subscriptionBillingCycleEditDeleteUnchanged. Also removes a concatenation. See Remove a concatenation.

Anchor to Deprecated and renamed fieldsDeprecated and renamed fields

The SubscriptionContractCalculation API introduces changes to how certain contract attributes are managed. The following fields have been deprecated, renamed, or moved.

Contract status is now managed using dedicated mutations instead of being set through the draft or contract calculation input. This change decouples contract status from the contract versioning model.

Old approachNew approach
Set status field in SubscriptionDraftInput during commit.Use dedicated status mutations: subscriptionContractActivate, subscriptionContractPause, subscriptionContractCancel, subscriptionContractExpire, subscriptionContractFail.

SubscriptionLineInput and SubscriptionLineUpdateInput map to SubscriptionContractCalculationProductVariantLineInput as follows. Three fields are renamed, and two have no draft API equivalent.

Draft API field (SubscriptionLineInput, SubscriptionLineUpdateInput)Calculate API field (SubscriptionContractCalculationProductVariantLineInput)Notes
—idThe ID of the existing line, read from SubscriptionLine.id. Include it to update a line in place. Omit it to add a new line.
productVariantIdproductVariantIdUnchanged.
quantityquantityUnchanged.
currentPrice (Decimal)priceOverride (MoneyInput)The per-unit price your app charges for the line. See Line price.
customAttributescustomAttributesOptional. Defaults to [] when omitted, which clears the line's custom attributes.
sellingPlanIdoriginSellingPlanIdUsed to find the line's delivery profile. It isn't applied as a selling plan during calculation.
sellingPlanNamesellingPlanNameUnchanged. When omitted, defaults to the current name of originSellingPlanId, or to no name when that's also omitted. Restate the stored name to keep it.
pricingPolicyappManagedPricingPolicyRenamed. Same metadata, with Money values instead of Decimal values. See Line pricing policy.
—discountsLine-scoped discounts. Required; pass [] for none. Replaces line-targeted subscriptionDraftDiscountAdd. See Line-scoped discounts.

A SubscriptionLineUpdateInput is a patch: a field you omit keeps its stored value. A SubscriptionContractCalculationProductVariantLineInput is a complete restatement of the line: a field you omit is cleared or recalculated, even when you include the line's id. To change one field on one line, restate every field of every line from the stored contract. Other contract fields, such as deliveryMethod or billingPolicy, can still be omitted and are preserved. See Preserve a line's state when you change it.

In the draft API, your app sets the line price with currentPrice. In the calculate API, the same price is priceOverride, a MoneyInput in the contract's currency. When you provide priceOverride, the calculation uses it as the line's per-unit price. When you omit it, the calculation prices the line from the product variant's current price, and any stored currentPrice is lost.

Old fieldNew field
currentPricepriceOverride
Omitted priceOverride reprices the line

A line that you restate without priceOverride is repriced from the variant. Always read currentPrice from the contract and pass it as priceOverride when you want to keep the price.

The pricingPolicy field on subscription lines is renamed to appManagedPricingPolicy. It's the drop-in replacement for the draft API field and carries the same data: a base price and the expected price adjustments per billing cycle.

In both APIs, this policy is metadata that your app stores on the line. It doesn't drive the calculated price. Your app remains responsible for setting the price it charges each cycle, through currentPrice in the draft API and through priceOverride in the calculate API. The name change makes that explicit.

Old fieldNew field
pricingPolicy.basePrice (Decimal)appManagedPricingPolicy.basePrice (MoneyInput)
pricingPolicy.cycleDiscountsappManagedPricingPolicy.cyclePriceAdjustments
cycleDiscounts[].afterCyclecyclePriceAdjustments[].afterCycle
cycleDiscounts[].adjustmentTypecyclePriceAdjustments[].adjustmentType
cycleDiscounts[].adjustmentValuecyclePriceAdjustments[].adjustmentValue
cycleDiscounts[].computedPrice (Decimal)cyclePriceAdjustments[].computedPrice (MoneyInput)

cyclePriceAdjustments must contain at least one entry. To restate a stored policy, read SubscriptionLine.pricingPolicy from the contract and map each cycleDiscounts entry to a cyclePriceAdjustments entry.

Omitted appManagedPricingPolicy clears the stored policy

Because a line input is a complete restatement, a line that you restate without appManagedPricingPolicy loses its stored pricing policy on commit. The calculation succeeds with no warning. Restate the policy on every line that has one.

Anchor to Billing configurationBilling configuration

The nextBillingDate, minCycles, and maxCycles fields have been moved into a new AppManagedBillingConfig object. This consolidates app-managed billing metadata that isn't used during contract calculation into a single location.

Old fieldsNew field
nextBillingDate, minCycles, maxCyclesappManagedBillingConfig.nextBillingDate, appManagedBillingConfig.minCycles, appManagedBillingConfig.maxCycles

The draft API sets the delivery charge with a standalone deliveryPrice field on SubscriptionDraftInput. The calculate API has no top-level deliveryPrice. The price moves onto the delivery method itself, so each committed method carries its own charge.

Old fieldNew field
SubscriptionDraftInput.deliveryPrice (Decimal)deliveryMethod.shipping.deliveryPrice, deliveryMethod.localDelivery.deliveryPrice, or deliveryMethod.pickup.deliveryPrice (MoneyInput)

An app-supplied delivery price is still possible, and delivery isn't recalculated when you commit a method:

  • deliveryPrice is required on shipping, localDelivery, and pickup. You can't provide one of these methods without it.
  • The calculation uses the deliveryPrice you provide as the contract's delivery charge. Delivery discounts in manualDiscounts[].deliveryDiscount apply on top of it.
  • When you omit deliveryMethod on an update, the contract keeps its existing delivery method, including its stored price.
  • To get Shopify's current rates for an address instead of supplying a price, use fetchAvailableDeliveryOptions, then commit the chosen option with its price as deliveryPrice. See Fetch delivery options.

Anchor to Multiple fulfillment configurationMultiple fulfillment configuration

Multi fulfillment is now optional

Passing multiFulfillment is only required for pre-paid subscriptions that have multiple deliveries per billing period. Omit the multi fulfillment input to use the billing cadence as the delivery cadence.

The SubscriptionContractCalculation API introduces explicit control over the number of fulfillments per billing cycle through a new multiFulfillment field. Previously, the number of deliveries per billing cycle was implicitly derived from the relationship between billing and delivery policy intervals. The new approach makes this configuration explicit.

The multiFulfillment field accepts a SubscriptionMultipleFulfillmentConfigInput with the following fields:

FieldTypeRequiredDescription
cadenceObjectYesThe cadence for calculating fulfillment dates.
numberOfFulfillmentsIntegerYesThe number of fulfillments per billing cycle. Must be at least 2.

Use the GraphQL Admin API docs to explore the new API mutations, input types, and return types.


Anchor to Create a subscription contractCreate a subscription contract

Anchor to Old approach (SubscriptionDraft)Old approach (SubscriptionDraft)

With the SubscriptionDraft API, creating a contract requires multiple sequential mutations:

  1. Create a draft for the new contract using subscriptionContractCreate.
  2. Add lines to the draft using subscriptionDraftLineAdd.
  3. Commit the draft to create the contract using subscriptionDraftCommit.

Anchor to New approach (SubscriptionContractCalculation)New approach (SubscriptionContractCalculation)

With the SubscriptionContractCalculation API, you submit the complete desired state for the new contract in a single mutation. The server calculates pricing, applies taxes, and returns an immutable result snapshot.

  1. Submit the new contract's desired state to create a contract calculation:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Create contract calculation

    mutation CreateCalculation {
    subscriptionContractCreateCalculate(
    contractCreateInput: {
    # Control contract calculation behavior
    withMerchandiseCustomizations: true

    # Customer to create the contract for (must belong to the current shop)
    customerId: "gid://shopify/Customer/123"

    # Currency for the contract
    currencyCode: USD

    # Define all lines (at least one required)
    lines: [
    {
    productVariantLine: {
    productVariantId: "gid://shopify/ProductVariant/111"
    quantity: 1
    customAttributes: []
    discounts: [] # Line-scoped discounts; pass [] for none
    }
    }
    ]

    # Define billing policy
    billingPolicy: {
    cadence: {
    unit: MONTH
    count: 1
    }
    }

    # Define delivery policy
    deliveryPolicy: {
    multiFulfillment: {
    cadence: { unit: WEEK, count: 2 }
    numberOfFulfillments: 2
    }
    }

    # Define delivery method (use { none: true } for digital-only subscriptions)
    deliveryMethod: {
    shipping: {
    address: {
    firstName: "Quinn"
    lastName: "Ishida"
    address1: "123 Main St"
    city: "Toronto"
    provinceCode: "ON"
    countryCode: CA
    zip: "M5V 1A1"
    }
    deliveryPrice: {
    amount: "5.00"
    currencyCode: USD
    }
    }
    }

    # Define payment method (use { none: true } for no payment method)
    paymentMethod: {
    customerPaymentMethod: {
    id: "gid://shopify/CustomerPaymentMethod/456"
    }
    }

    # Define discount codes to apply (pass an empty array for none)
    discountCodes: []

    # Define manual discounts (pass an empty array for no manual discounts)
    # Supports two types: orderDiscount, deliveryDiscount
    manualDiscounts: []

    # Define custom attributes (pass an empty array for none)
    customAttributes: []
    }
    ) {
    subscriptionContractCalculation {
    ... on SubscriptionContractCalculationPending {
    id
    }
    }
    userErrors {
    field
    message
    code
    }
    }
    }

    JSON response

    {
    "data": {
    "subscriptionContractCreateCalculate": {
    "subscriptionContractCalculation": {
    "id": "gid://shopify/SubscriptionContractCalculation/789"
    },
    "userErrors": []
    }
    }
    }
  2. Poll for the contract calculation result:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Poll for result

    query PollCalculation {
    subscriptionContractCalculation(
    id: "gid://shopify/SubscriptionContractCalculation/789"
    ) {
    __typename
    ... on SubscriptionContractCalculationPending {
    id
    }
    ... on SubscriptionContractCalculationSuccess {
    id
    calculatedContract {
    lines(first: 10) {
    edges {
    node {
    id
    variantId
    quantity
    }
    }
    }
    }
    }
    ... on SubscriptionContractCalculationFailure {
    id
    errors { code }
    }
    }
    }

    JSON response (pending)

    {
    "data": {
    "subscriptionContractCalculation": {
    "__typename": "SubscriptionContractCalculationPending",
    "id": "gid://shopify/SubscriptionContractCalculation/789"
    }
    }
    }
  3. Commit the contract calculation to create the contract:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Commit contract calculation

    mutation CommitCalculation {
    subscriptionContractCalculationCommit(
    id: "gid://shopify/SubscriptionContractCalculation/789"
    ) {
    # The return type is a union: SubscriptionContract (create/update)
    # or SubscriptionBillingCycleEditedContract (billing cycle edit).
    contract {
    ... on SubscriptionContract {
    id
    status
    }
    }
    userErrors {
    field
    message
    }
    }
    }

    JSON response

    {
    "data": {
    "subscriptionContractCalculationCommit": {
    "contract": {
    "id": "gid://shopify/SubscriptionContract/999",
    "status": "ACTIVE"
    },
    "userErrors": []
    }
    }
    }

Anchor to Update a subscription contractUpdate a subscription contract

Anchor to Old approach (SubscriptionDraft)Old approach (SubscriptionDraft)

With the SubscriptionDraft API, updating a contract requires multiple sequential mutations:

  1. Create a draft from the existing contract using subscriptionContractUpdate.
  2. Add, update, or remove lines using subscriptionDraftLineAdd, subscriptionDraftLineUpdate, or subscriptionDraftLineRemove.
  3. Apply discounts using subscriptionDraftDiscountCodeApply, subscriptionDraftDiscountAdd, or subscriptionDraftFreeShippingDiscountAdd.
  4. Commit the draft to apply changes using subscriptionDraftCommit.

Anchor to New approach (SubscriptionContractCalculation)New approach (SubscriptionContractCalculation)

With the SubscriptionContractCalculation API, you submit only the top-level fields you want to change in a single mutation. Omitted top-level fields are left unchanged. A list you provide, such as lines, replaces the stored list.

Fetch the existing SubscriptionContract state using the subscriptionContract query, then apply any changes before submitting an updated version of the SubscriptionContract attributes for contract calculation.

  1. Submit the desired state to create a contract calculation. On an update, only withMerchandiseCustomizations must be provided; all other top-level fields are optional, and omitted top-level fields are left unchanged:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Create contract calculation

    mutation UpdateCalculation {
    subscriptionContractUpdateCalculate(
    contractId: "gid://shopify/SubscriptionContract/123"
    contractUpdateInput: {
    # Control contract calculation behavior
    withMerchandiseCustomizations: true

    # Define all lines (replaces existing lines when provided)
    lines: [
    {
    productVariantLine: {
    # Include id to keep existing line, and restate its stored
    # fields: an omitted priceOverride reprices the line from
    # the variant, and an omitted appManagedPricingPolicy
    # clears the stored policy.
    id: "gid://shopify/SubscriptionLine/existing-line-uuid"
    productVariantId: "gid://shopify/ProductVariant/111"
    quantity: 1
    priceOverride: { amount: "8.02", currencyCode: GBP }
    customAttributes: []
    discounts: [] # Line-scoped discounts; pass [] for none
    }
    }
    {
    productVariantLine: {
    # Omit id to add new line
    productVariantId: "gid://shopify/ProductVariant/789"
    quantity: 2
    customAttributes: []
    discounts: []
    }
    }
    ]

    # Define discount codes to apply (resolved into manual discounts; not persisted as codes)
    discountCodes: [
    { redeemCode: "SAVE10" }
    ]

    # Define all manual discounts (replaces existing manual discounts)
    # Supports two types: orderDiscount, deliveryDiscount
    manualDiscounts: [
    {
    orderDiscount: {
    # Include id to keep an existing order discount
    id: "gid://shopify/SubscriptionDiscount/existing-discount-uuid"
    title: "10% off subscription"
    value: {
    percentage: 10
    }
    recurringCycleLimit: 0 # 0 = no limit
    }
    }
    {
    deliveryDiscount: {
    # Omit id for a new delivery discount
    title: "Free shipping for 3 months"
    value: {
    percentage: 100
    }
    recurringCycleLimit: 3 # Apply for 3 billing cycles
    }
    }
    ]

    # Define billing policy (replaces existing billing policy)
    billingPolicy: {
    anchors: [
    {
    monthday: {
    dayOfMonth: 7
    }
    }
    ]
    cadence: {
    unit: MONTH
    count: 2
    }
    }

    # Define delivery policy (replaces existing delivery policy)
    deliveryPolicy: {
    anchors: {
    weekday: {
    dayOfWeek: TUESDAY
    }
    }
    multiFulfillment: {
    cadence: { unit: WEEK, count: 1 }
    numberOfFulfillments: 8
    }
    }

    # Define delivery method (replaces existing delivery method when provided)
    deliveryMethod:{
    pickup: {
    title: "Test",
    locationId: "gid://shopify/Location/1234",
    deliveryPrice: {
    amount: "0",
    currencyCode: GBP
    }
    }
    }

    # Define payment method (replaces existing payment method when provided)
    paymentMethod: {
    customerPaymentMethod: {
    id: "gid://shopify/CustomerPaymentMethod/1234"
    }
    }

    # Define note (replaces existing note)
    note: "Test"
    }
    ) {
    subscriptionContractCalculation {
    ... on SubscriptionContractCalculationPending {
    id
    }
    }
    userErrors {
    field
    message
    }
    }
    }

    JSON response

    {
    "data": {
    "subscriptionContractUpdateCalculate": {
    "subscriptionContractCalculation": {
    "id": "gid://shopify/SubscriptionContractCalculation/789"
    },
    "userErrors": []
    }
    }
    }
  2. Poll for the contract calculation result:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Poll for result

    query PollCalculation {
    subscriptionContractCalculation(
    id: "gid://shopify/SubscriptionContractCalculation/789"
    ) {
    __typename
    ... on SubscriptionContractCalculationPending {
    id
    }
    ... on SubscriptionContractCalculationSuccess {
    id
    calculatedContract {
    lines(first: 10) {
    edges {
    node {
    id
    variantId
    quantity
    }
    }
    }
    }
    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 }
    }
    }
    }

    JSON response (pending)

    {
    "data": {
    "subscriptionContractCalculation": {
    "__typename": "SubscriptionContractCalculationPending",
    "id": "gid://shopify/SubscriptionContractCalculation/789"
    }
    }
    }
  3. Commit the contract calculation to apply changes:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Commit contract calculation

    mutation CommitCalculation {
    subscriptionContractCalculationCommit(
    id: "gid://shopify/SubscriptionContractCalculation/789"
    ) {
    # The return type is a union: SubscriptionContract (create/update)
    # or SubscriptionBillingCycleEditedContract (billing cycle edit).
    contract {
    ... on SubscriptionContract {
    id
    status
    }
    }
    userErrors {
    field
    message
    }
    }
    }

    JSON response

    {
    "data": {
    "subscriptionContractCalculationCommit": {
    "contract": {
    "id": "gid://shopify/SubscriptionContract/123",
    "status": "ACTIVE"
    },
    "userErrors": []
    }
    }
    }

Anchor to Preserve a line's state when you change itPreserve a line's state when you change it

The lines array replaces all existing lines, and each productVariantLine entry is a complete restatement of that line, even when you include its id. Including the id keeps the line's identity but it doesn't carry over its stored fields. Any field you omit from the entry is cleared or recalculated, and the calculation succeeds with no warning. To change one field on one line, read every line from the contract and restate every field of every line, as described in Line fields.


Anchor to Edit a single billing cycleEdit a single billing cycle

Apply a one-time change to a single billing cycle of a subscription without changing the recurring contract. Later cycles continue to bill from the underlying contract. A billing cycle can contain more than one delivery (for example, prepaid or multi-fulfillment subscriptions), and the edit applies to the whole cycle.

Anchor to Old approach (SubscriptionDraft)Old approach (SubscriptionDraft)

With the SubscriptionDraft object, editing one billing cycle requires multiple sequential mutations:

  1. Create a draft scoped to the target cycle using subscriptionBillingCycleContractEdit.
  2. Add, update, or remove lines and discounts on the draft using the subscriptionDraft* mutations.
  3. Commit the draft using subscriptionBillingCycleContractDraftCommit.

Anchor to New approach (SubscriptionContractCalculation)New approach (SubscriptionContractCalculation)

With the SubscriptionContractCalculation API, you submit the complete desired state for the cycle in a single mutation and identify the cycle with a selector, then poll and commit exactly as you do for a contract update.

  1. Submit the desired state for the target cycle to create a contract calculation, identifying the cycle with a billingCycleSelector. Select the cycle either:

    1. By its index:

      mutation {
      subscriptionBillingCycleContractEditCalculate(
      contractId: "gid://shopify/SubscriptionContract/1"
      billingCycleSelector: { index: 2 }
      billingCycleEditInput: {
      withMerchandiseCustomizations: true,
      # The complete desired state for this cycle (lines, discounts, delivery, and so on).
      lines: [
      {
      productVariantLine: {
      productVariantId: "gid://shopify/ProductVariant/1"
      quantity: 2
      customAttributes: []
      discounts: [] # Line-scoped discounts; pass [] for none
      }
      }
      ]
      }
      ) {
      subscriptionContractCalculation {
      ... on SubscriptionContractCalculationPending {
      id
      }
      }
      userErrors {
      field
      message
      }
      }
      }
    2. Or by a date that falls within the cycle:

      billingCycleSelector: { date: "2025-06-01T00:00:00Z" }
  2. Poll the calculation and commit it as described in Update a subscription contract. You wait for the calculation to succeed, then commit it with subscriptionContractCalculationCommit.

Committing creates and commits a new contract version scoped to that single billing cycle, leaving the recurring contract unchanged.


Anchor to Concatenate billing cyclesConcatenate billing cycles

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 of the concatenated cycles creates one order that's linked to every contract in the group. For the background on why you'd do this, see Combine subscription contracts.

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.

Anchor to What's different about concatenationWhat's different about concatenation

A concatenation is a billing cycle edit that spans several contracts. It's scoped to the selected billing cycles only:

  • The recurring contracts don't change. Committing the calculation applies the result to the selected cycle of each contract. Every contract keeps its own lines, policies, payment method, and next billing date for the cycles that follow.
  • You describe the whole order. The draft API merges the lines and discounts of every concatenated contract into the draft for you. The calculate API doesn't merge anything: lines, manualDiscounts, and discountCodes are the complete order, and nothing is inherited from any contract. A line you leave out isn't billed.
  • Contract-level fields aren't part of the input. The draft API accepts contract-level fields on the draft, such as paymentMethodId. The concatenate input has none of these. The payment method, billing policy, and delivery policy come from the contract you concatenate into, and status and next billing date stay on each recurring contract.
  • The delivery method is required. The combined order ships as one, so you provide its delivery method instead of inheriting one.

The calculate API concatenates at billing cycle scope only. To merge contracts permanently, add the other contract's lines to one contract with subscriptionContractUpdateCalculate as described in Update a subscription contract, and then cancel the contract you merged.

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

The following table maps each step of the draft concatenation flow to subscriptionBillingCycleContractConcatenateCalculate.

Draft APICalculate API
subscriptionBillingCycleContractEdit on the first contract's cycleThe contractId and billingCycleSelector arguments.
subscriptionBillingCycleContractDraftConcatenate with concatenatedBillingCycleContractsThe concatenatedBillingCycles field on billingCycleConcatenateInput.
Lines merged from every contract automaticallylines[]. Restate the anchor's lines with their id, and the members' lines with originLineId. See Carry lines over from each contract.
subscriptionDraftDiscountAdd, subscriptionDraftDiscountCodeApply, subscriptionDraftFreeShippingDiscountAddmanualDiscounts[], discountCodes[], and lines[].productVariantLine.discounts[]. See Manage discounts.
subscriptionDraftUpdate with deliveryMethod and deliveryPricedeliveryMethod. Required. See Delivery price.
subscriptionDraftUpdate with note and customAttributesnote and customAttributes. Not inherited from the anchor.
subscriptionDraftUpdate with paymentMethodId, status, or nextBillingDateNot available. These belong to the recurring contracts.
subscriptionBillingCycleContractDraftCommitsubscriptionContractCalculationCommit.
subscriptionBillingCycleEditDeleteUnchanged. See Remove a concatenation.

Anchor to Requirements on the cyclesRequirements on the cycles

The calculation is rejected with a userErrors entry when any of the following isn't true. At commit, Shopify locks the group and checks the requirements again against the cycles' state at that moment. If a cycle was billed, skipped, or joined another concatenation in between, or a member contract no longer meets the requirements, the commit returns a userErrors entry with code STALE_CONTRACT. Recalculate and commit the new calculation. A contract edit made on a member cycle in between doesn't make the commit stale: the commit replaces that edit with the concatenated edit. Schedule edits on the cycles stay in place.

  • No cycle in the group has ended, is skipped, has a billing attempt that hasn't failed, or is already part of a concatenation. To change a group, remove the concatenation first and create a new one.
  • 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.
  • concatenatedBillingCycles has at least one entry and at most 50, doesn't include the anchor, and names each contract once. To edit one cycle on its own, use subscriptionBillingCycleContractEditCalculate.

A member cycle that already has its own billing cycle edit is accepted. Committing the concatenation supersedes that edit, because the concatenate input is the complete order.

Anchor to Old approach (SubscriptionDraft)Old approach (SubscriptionDraft)

With the SubscriptionDraft API, concatenating contracts requires multiple sequential mutations:

  1. Create a draft scoped to one contract's cycle using subscriptionBillingCycleContractEdit.
  2. Join the other contracts' cycles to the draft using subscriptionBillingCycleContractDraftConcatenate. Their lines and discounts are merged into the draft.
  3. Optionally, update the draft using the subscriptionDraft* mutations.
  4. Commit the draft using subscriptionBillingCycleContractDraftCommit.

Anchor to New approach (SubscriptionContractCalculation)New approach (SubscriptionContractCalculation)

With the SubscriptionContractCalculation API, you submit the anchor, the member cycles, and the complete order in a single mutation, then poll and commit exactly as you do for a billing cycle edit.

  1. Submit the concatenation to create a contract calculation. Identify the anchor cycle with contractId and a billingCycleSelector, and each member cycle with a SubscriptionBillingCycleInput:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Create concatenation calculation

    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: []
    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: []
    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

    {
    "data": {
    "subscriptionBillingCycleContractConcatenateCalculate": {
    "subscriptionContractCalculation": {
    "id": "gid://shopify/SubscriptionContractCalculation/789"
    },
    "userErrors": []
    }
    }
    }
  2. Poll for the contract calculation result as described in Update a subscription contract. The SubscriptionContractCalculationSuccess result carries the projectedOrderTotals for the combined order.

  3. Commit the contract calculation. For a concatenation, the contract field in the response is a SubscriptionBillingCycleEditedContract, and its billingCycles connection lists every cycle in the group:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Commit concatenation calculation

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

    JSON response

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

After the commit, the subscriptionBillingCycle query for any cycle in the group returns the same editedContract, with edited: true. Its billingCycles connection lists the whole group, so you can read group membership from any member. This is the same query you use after a draft concatenation.

Anchor to Carry lines over from each contractCarry lines over from each contract

Because the calculate API doesn't merge lines, you build the combined lines array yourself. Read the lines of each contract's cycle from subscriptionBillingCycle.editedContract (or from the contract when the cycle has no edit), then restate each one as described in Line fields. Two fields identify where a line comes from:

Line belongs toField to setEffect
The anchor contractidKeeps the line's identity, as on a billing cycle edit.
A member contractoriginLineIdRecords the member contract on the resulting order line, so that contract still shows the order it was billed in. Omit id.
No contract (new)NeitherAdds a new line to the combined order.

originLineId must name a line on one of the member contracts, not on the anchor, and can't be combined with id.

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.

Anchor to Bill the concatenated cyclesBill the concatenated cycles

Billing doesn't change. When the billing date comes, create one billing attempt on any contract in the group with subscriptionBillingAttemptCreate or subscriptionBillingCycleCharge, selecting that contract's concatenated cycle. Shopify creates one order that's linked to every contract in the group and marks every cycle in the group as billed.

A billing attempt on a concatenated cycle can return the following BillingAttemptUserError codes in addition to the usual ones:

CodeMeaning
BILLING_CYCLE_GROUP_ALREADY_ATTEMPTEDA billing cycle concatenated with this one has already been billed, or has a billing attempt that hasn't finished. Wait for any unfinished attempt to finish and check its result. To charge the cycles separately, remove the concatenation first.
BILLING_CYCLE_GROUP_CYCLE_SKIPPEDA billing cycle concatenated with this one is skipped.
BILLING_CYCLE_GROUP_CONTENDEDAnother process is modifying a billing cycle concatenated with this one. Retry.

Anchor to Remove a concatenationRemove a concatenation

To dissolve a group before it bills, call subscriptionBillingCycleEditDelete on any one cycle in the group. This is the same mutation the draft API uses. It removes the shared edit from every cycle in the group in one operation, and each cycle goes back to billing from its own recurring contract:

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Remove concatenation

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

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

The response lists every cycle that was in the group. A second call on any of them returns a NO_CYCLE_EDITS error, because the cycles no longer carry an edit.

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 if you still need it.

Remove a concatenation before you bill it. While a billing attempt is in progress on the cycle you target, the mutation returns an INCOMPLETE_BILLING_ATTEMPTS error. To change the members or the lines of a group you've committed, remove the concatenation and then submit a new subscriptionBillingCycleContractConcatenateCalculate.

A calculation you haven't committed doesn't lock the cycles. To change the members or the lines before you commit, submit a new calculation and commit that one instead. Shopify deletes the calculations you don't commit after seven days, as described in Contract calculation states.


The SubscriptionContractCalculation API splits discounts into two separate input fields that replace the multiple draft-based discount mutations:

  • discountCodes[]: Discount codes to apply during the calculation. Applied codes are resolved into manual discounts on the resulting contract; codes aren't persisted as codes, so there's nothing to preserve or replace across calculations. Defaults to an empty array.
  • manualDiscounts[]: Manual order and delivery discounts. This is a @oneOf input — each entry is either an orderDiscount or a deliveryDiscount.

For line-scoped discounts (discounts that apply to a single line), use the discounts[] field on each line input. See Line-scoped discounts.

Manual discounts replacement behavior

The manualDiscounts array replaces all existing manual discounts on the contract. To keep an existing discount, include it in the array with its id. To remove a discount, omit it from the array. To add a new discount, include it without an id. To remove all manual discounts, pass an empty array ([]).

Bundles and omitted lines

The order and delivery discount examples omit lines and set withMerchandiseCustomizations: false so the calculation leaves the contract's lines and bundles alone. With true, an omitted lines rebuilds expanded and merged bundles and fails on a custom bundle. The line-scoped example supplies lines, so it sets true.

Discount typeOld mutation(s)New input fieldDescription
Code discountsubscriptionDraftDiscountCodeApplydiscountCodes[]Apply a discount code by its redeem code.
Order discountsubscriptionDraftDiscountAdd, subscriptionDraftDiscountUpdate, subscriptionDraftDiscountRemovemanualDiscounts[].orderDiscountApply a fixed or percentage discount to all lines.
Delivery discountsubscriptionDraftFreeShippingDiscountAdd, subscriptionDraftFreeShippingDiscountUpdatemanualDiscounts[].deliveryDiscountApply a discount to shipping or delivery charges.
Line-scoped discountsubscriptionDraftDiscountAdd (line-scoped)lines[].productVariantLine.discounts[]Apply a fixed or percentage discount to a single line.

Anchor to Apply a discount codeApply a discount code

To apply a discount code to an existing contract, include an entry in the discountCodes array:

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Apply code discount

mutation ApplyCodeDiscount {
subscriptionContractUpdateCalculate(
contractId: "gid://shopify/SubscriptionContract/123"
contractUpdateInput: {
withMerchandiseCustomizations: false
discountCodes: [
{ redeemCode: "SAVE10" }
]
}
) {
subscriptionContractCalculation {
... on SubscriptionContractCalculationPending {
id
}
}
userErrors {
field
message
}
}
}

Anchor to Apply an order discountApply an order discount

Order discounts apply to all lines on the contract. They support percentage or fixed amount values:

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Percentage order discount

mutation ApplyOrderDiscount {
subscriptionContractUpdateCalculate(
contractId: "gid://shopify/SubscriptionContract/123"
contractUpdateInput: {
withMerchandiseCustomizations: false
manualDiscounts: [
{
orderDiscount: {
title: "10% loyalty discount"
value: {
percentage: 10
}
recurringCycleLimit: 0 # 0 = applies indefinitely
}
}
]
}
) {
subscriptionContractCalculation {
... on SubscriptionContractCalculationPending {
id
}
}
userErrors {
field
message
}
}
}

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Fixed amount order discount

mutation ApplyFixedOrderDiscount {
subscriptionContractUpdateCalculate(
contractId: "gid://shopify/SubscriptionContract/123"
contractUpdateInput: {
withMerchandiseCustomizations: false
manualDiscounts: [
{
orderDiscount: {
title: "$5 off subscription"
value: {
fixedAmount: {
appliesOnEachItem: true
amount: { amount: "5.00", currencyCode: USD }
}
}
recurringCycleLimit: 3 # Applies for 3 billing cycles
}
}
]
}
) {
subscriptionContractCalculation {
... on SubscriptionContractCalculationPending {
id
}
}
userErrors {
field
message
}
}
}

Anchor to Apply a delivery discountApply a delivery discount

Delivery discounts apply to shipping or delivery charges. For example, to offer free shipping:

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Free shipping discount

mutation ApplyDeliveryDiscount {
subscriptionContractUpdateCalculate(
contractId: "gid://shopify/SubscriptionContract/123"
contractUpdateInput: {
withMerchandiseCustomizations: false
manualDiscounts: [
{
deliveryDiscount: {
title: "Free shipping"
value: {
percentage: 100
}
recurringCycleLimit: 0 # 0 = free shipping indefinitely
}
}
]
}
) {
subscriptionContractCalculation {
... on SubscriptionContractCalculationPending {
id
}
}
userErrors {
field
message
}
}
}

Anchor to Combine multiple discountsCombine multiple discounts

You can apply multiple discounts of different types in a single calculation. Include discount codes in discountCodes[] and manual discounts in manualDiscounts[]:

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Multiple discount types

mutation ApplyMultipleDiscounts {
subscriptionContractUpdateCalculate(
contractId: "gid://shopify/SubscriptionContract/123"
contractUpdateInput: {
withMerchandiseCustomizations: false
discountCodes: [
{ redeemCode: "SAVE10" }
]
manualDiscounts: [
{
orderDiscount: {
title: "Loyalty reward"
value: { percentage: 5 }
recurringCycleLimit: 0
}
}
{
deliveryDiscount: {
title: "Free shipping promo"
value: { percentage: 100 }
recurringCycleLimit: 6
}
}
]
}
) {
subscriptionContractCalculation {
... on SubscriptionContractCalculationPending {
id
}
}
userErrors {
field
message
}
}
}

Anchor to Line-scoped discountsLine-scoped discounts

To apply a discount to a single line, include it in the discounts[] field on that line's input. Each line-scoped discount takes title, value, and recurringCycleLimit fields, and optionally an id to update an existing line discount:

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Line-scoped discount

mutation ApplyLineDiscount {
subscriptionContractUpdateCalculate(
contractId: "gid://shopify/SubscriptionContract/123"
contractUpdateInput: {
withMerchandiseCustomizations: true
lines: [
{
productVariantLine: {
id: "gid://shopify/SubscriptionLine/line-uuid-1"
productVariantId: "gid://shopify/ProductVariant/111"
quantity: 1
customAttributes: []
discounts: [
{
title: "10% off this line"
value: { percentage: 10 }
recurringCycleLimit: 0
}
]
}
}
]
}
) {
subscriptionContractCalculation {
... on SubscriptionContractCalculationPending {
id
}
}
userErrors {
field
message
}
}
}

Anchor to Remove all discountsRemove all discounts

To remove all manual discounts from a contract, pass an empty manualDiscounts array and an empty discountCodes array:

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Remove all discounts

mutation RemoveAllDiscounts {
subscriptionContractUpdateCalculate(
contractId: "gid://shopify/SubscriptionContract/123"
contractUpdateInput: {
withMerchandiseCustomizations: false
manualDiscounts: []
discountCodes: []
}
) {
subscriptionContractCalculation {
... on SubscriptionContractCalculationPending {
id
}
}
userErrors {
field
message
}
}
}

Anchor to Fetch delivery optionsFetch delivery options

The deliveryOptions field on SubscriptionContractCalculationSuccess returns the available delivery options that the calculation computed. This field replaces the SubscriptionDraft.deliveryOptions field from the SubscriptionDraft API, and returns all available options.

The SubscriptionContractCalculationSuccess type also exposes:

  • warnings: Non-fatal warnings produced during the calculation. The calculation still succeeded; warnings inform the merchant about issues that should be reviewed before committing.
  • projectedOrderTotals: Projected order totals (subtotal, total delivery, estimated tax, merchandise discounts, delivery discounts, and grand total) for the calculated contract. Null when the calculation only discovered delivery options for an address rather than calculating committed totals.

Each option is a SubscriptionContractCalculationDeliveryOption, which resolves to one of the following types:

  • SubscriptionContractCalculationShippingOption
  • SubscriptionContractCalculationLocalDeliveryOption
  • SubscriptionContractCalculationPickupOption

Fetching delivery options is opt-in. A calculation that sets or keeps a committed delivery method uses a faster path that doesn't look up the full set of rates, so it doesn't return the complete set of options. To fetch all available options for an address, provide fetchAvailableDeliveryOptions in the deliveryMethod input instead of a method. This is a discovery calculation. It doesn't change the contract's committed delivery method, and it can't be committed.

Caution

A discovery calculation can't be committed. subscriptionContractCalculationCommit returns the DISCOVERY_CALCULATION_NOT_COMMITTABLE user error. To commit a change, create a new calculation with the chosen delivery method, then commit it.

Note

deliveryMethod is a @oneOf input. Provide exactly one of shipping, localDelivery, pickup, none, or fetchAvailableDeliveryOptions. Providing a committed method and fetchAvailableDeliveryOptions in the same calculation is rejected.

Anchor to Old approach (SubscriptionDraft)Old approach (SubscriptionDraft)

With the SubscriptionDraft API, fetching and setting a delivery option requires multiple sequential mutations and a separate asynchronous query:

  1. Create a draft using subscriptionContractUpdate (or subscriptionContractCreate).
  2. Query the deliveryOptions field on the SubscriptionDraft with a deliveryAddress, polling until it returns a non-null result. Delivery option lookup is asynchronous and returns null while pending.
  3. Set the selected method using subscriptionDraftUpdate with a deliveryMethod.
  4. Commit the draft using subscriptionDraftCommit.

Anchor to New approach (SubscriptionContractCalculation)New approach (SubscriptionContractCalculation)

With the SubscriptionContractCalculation API, you fetch delivery options with a calculation, read them off the result, then create a new calculation with the chosen delivery method. Only that second calculation can be committed.

  1. Submit a contract calculation that fetches the delivery options by providing fetchAvailableDeliveryOptions in the deliveryMethod input:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Fetch delivery options

    mutation FetchDeliveryOptions {
    subscriptionContractUpdateCalculate(
    contractId: "gid://shopify/SubscriptionContract/123"
    contractUpdateInput: {
    withMerchandiseCustomizations: true

    # Fetch delivery options for this address without committing a method
    deliveryMethod: {
    fetchAvailableDeliveryOptions: {
    # Run delivery customization functions while discovering options (default)
    withDeliveryCustomizations: true

    address: {
    firstName: "Quinn"
    lastName: "Ishida"
    address1: "123 Main St"
    city: "Toronto"
    provinceCode: "ON"
    countryCode: CA
    zip: "M5V 1A1"
    }
    }
    }
    }
    ) {
    subscriptionContractCalculation {
    ... on SubscriptionContractCalculationPending {
    id
    }
    }
    userErrors {
    field
    message
    }
    }
    }

    JSON response

    {
    "data": {
    "subscriptionContractUpdateCalculate": {
    "subscriptionContractCalculation": {
    "id": "gid://shopify/SubscriptionContractCalculation/789"
    },
    "userErrors": []
    }
    }
    }
  2. Poll for the contract calculation result (see the recommended polling strategy) and read the deliveryOptions. Each option type is part of a union, so use inline fragments to select its fields:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Read delivery options

    query PollDeliveryOptions {
    subscriptionContractCalculation(
    id: "gid://shopify/SubscriptionContractCalculation/789"
    ) {
    __typename
    ... on SubscriptionContractCalculationPending {
    id
    }
    ... on SubscriptionContractCalculationSuccess {
    id
    deliveryOptions {
    __typename
    ... on SubscriptionContractCalculationShippingOption {
    title
    code
    price { amount currencyCode }
    }
    ... on SubscriptionContractCalculationLocalDeliveryOption {
    title
    code
    phoneRequired
    price { amount currencyCode }
    }
    ... on SubscriptionContractCalculationPickupOption {
    title
    code
    pickupTime
    price { amount currencyCode }
    location { id name }
    }
    }
    }
    ... on SubscriptionContractCalculationFailure {
    id
    errors { code }
    }
    }
    }

    JSON response (success)

    {
    "data": {
    "subscriptionContractCalculation": {
    "__typename": "SubscriptionContractCalculationSuccess",
    "id": "gid://shopify/SubscriptionContractCalculation/789",
    "deliveryOptions": [
    {
    "__typename": "SubscriptionContractCalculationShippingOption",
    "title": "Standard",
    "code": "Standard",
    "price": { "amount": "5.00", "currencyCode": "CAD" }
    },
    {
    "__typename": "SubscriptionContractCalculationShippingOption",
    "title": "Express",
    "code": "Express",
    "price": { "amount": "15.00", "currencyCode": "CAD" }
    }
    ]
    }
    }
    }
  3. Submit a second contract calculation that commits the selected option as the deliveryMethod. Map the chosen option's title and code directly, and map its price to the method's deliveryPrice field:

    POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

    Select a delivery option

    mutation SelectDeliveryOption {
    subscriptionContractUpdateCalculate(
    contractId: "gid://shopify/SubscriptionContract/123"
    contractUpdateInput: {
    withMerchandiseCustomizations: true

    # Commit the option chosen from deliveryOptions
    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
    }
    }
    }
    }
    ) {
    subscriptionContractCalculation {
    ... on SubscriptionContractCalculationPending {
    id
    }
    }
    userErrors {
    field
    message
    }
    }
    }
  4. Poll for the second contract calculation result, then commit it with subscriptionContractCalculationCommit to apply the selected delivery method to the contract. This follows the same poll-then-commit pattern as the create and update flows above.

Anchor to Delivery customization functionsDelivery customization functions

When a calculation fetches delivery options, the withDeliveryCustomizations field on fetchAvailableDeliveryOptions controls whether delivery customization functions run:

  • true (the default): Functions run, so the returned deliveryOptions reflect any options the merchant's customizations hide, rename, or re-order.
  • false: Functions are bypassed, and the raw delivery options are returned.

The SubscriptionDraft API doesn't run delivery customization functions when computing deliveryOptions. For shops with active delivery customizations, the set of options returned by the new API can therefore differ from the old field.

Note

withDeliveryCustomizations applies only to calculations that include fetchAvailableDeliveryOptions. Calculations that set a committed delivery method and recurring billing attempts always bypass delivery customization functions, because the contract's committed delivery method is fixed and re-running functions can remove it.


Anchor to Input types referenceInput types reference

Exploring input types

Explore the API to understand the full input schema, starting from the top-level input types.

Anchor to Top-level input typesTop-level input types

Input typeDescription
SubscriptionContractCalculationContractCreateInputInputs for creating a new subscription contract.
SubscriptionContractCalculationContractUpdateInputInput for updating an existing subscription contract.
SubscriptionContractCalculationBillingCycleEditInputInput for editing a single billing cycle of an existing contract.
SubscriptionContractCalculationBillingCycleConcatenateInputInput for concatenating the billing cycles of several contracts into one order. See Concatenate billing cycles.

Note

The inputs for lines, manualDiscounts, and deliveryMethod fields use the GraphQL @oneOf directive. You must provide exactly one of the available input options for each entry. discountCodes is a separate non-union array. Refer to the following sections for the specific options available for each field.

Anchor to [object Object]SubscriptionContractCalculationLineInput

Lines use a @oneOf input pattern. Provide exactly one of the following options:

  • productVariantLine: For product variant lines.
  • customLine: For custom lines without a variant.
  • productVariantParentLine: For product variant bundle parent lines (available when bundle support is enabled).
Lines replacement behavior

The lines array replaces all existing lines. Include all lines that you want to keep and omit any that you want to remove. Use line IDs to identify existing lines for updates. Each entry is a complete restatement of the line, not a patch: to keep an existing line unchanged, restate all of its fields from the stored contract. See Preserve a line's state when you change it.

Anchor to [object Object]SubscriptionContractCalculationProductVariantLineInput

Product variant lines accept the following fields:

FieldTypeRequiredDescription
idIDNoThe ID of an existing line to update. Omit for new lines.
productVariantIdIDYesThe ID of the product variant for this line.
quantityIntegerYesThe quantity of the product variant. Must be at least 1.
priceOverrideMoneyInputNoThe per-unit price your app charges for the line. Replaces the draft API's currentPrice. When omitted, the line is priced from the product variant's current price.
customAttributes[AttributeInput]No (default [])Custom attributes for this subscription line. Omitting it clears the line's custom attributes.
appManagedPricingPolicySubscriptionContractCalculationAppManagedPricingPolicyInputNoApp-managed pricing policy metadata for this line. Replaces the draft API's pricingPolicy. Doesn't drive the calculated price. When omitted on a restated line, the stored policy is cleared.
originSellingPlanIdIDNoThe ID of the selling plan this line was originally created with. Replaces the draft API's sellingPlanId. Used to find the delivery profile.
sellingPlanNameStringNoThe selling plan name for this line. When omitted, defaults to the current name of originSellingPlanId, or to no name when that's also omitted.
discounts[SubscriptionContractCalculationLineDiscountInput]YesLine-scoped manual discounts. Pass [] for none.
originLineIdIDNoConcatenation only. The line on a concatenated contract that this line is carried over from. Can't be combined with id. Ignored outside subscriptionBillingCycleContractConcatenateCalculate. See Carry lines over from each contract.

Anchor to [object Object]SubscriptionContractCalculationManualDiscountInput

Manual discounts use a @oneOf input pattern. Provide exactly one of the following options:

  • orderDiscount: For order discounts that apply to all lines on the contract. Takes title, value, and recurringCycleLimit fields. Optionally takes an id to update an existing discount.
  • deliveryDiscount: For delivery or shipping discounts. Takes title, value, and recurringCycleLimit fields. Optionally takes an id to update an existing discount.
Manual discounts replacement behavior

The manualDiscounts array replaces all existing manual discounts. Include all discounts that you want to keep and omit any that you want to remove. Use discount IDs to identify existing discounts for updates.

Anchor to [object Object]SubscriptionContractCalculationDiscountCodeInput

Discount codes are provided in the separate discountCodes[] array (not part of manualDiscounts). Each entry takes:

  • redeemCode: The discount code string to apply.

Applied codes are resolved into manual discounts on the resulting contract; codes aren't persisted as codes, so there's nothing to preserve or replace across calculations.

Anchor to [object Object]SubscriptionContractCalculationDiscountValueInput

Discount values use a @oneOf input pattern. Provide exactly one of the following options:

  • percentage: An integer percentage value (0–100).
  • fixedAmount: A fixed money amount with appliesOnEachItem (boolean) and amount (MoneyInput).

Anchor to [object Object]SubscriptionContractCalculationDeliveryMethodInput

Delivery methods use a @oneOf input pattern. Provide exactly one of the following options:

  • shipping: For shipping deliveries.
  • localDelivery: For local delivery.
  • pickup: For pickup.
  • none: Explicitly set no delivery method (digital-only subscriptions). Must be true.
  • fetchAvailableDeliveryOptions: To fetch the available delivery options for an address without committing a delivery method. The resulting discovery calculation can't be committed. See Fetch delivery options.

shipping, localDelivery, and pickup each require a deliveryPrice (MoneyInput). This is where the draft API's deliveryPrice lives. See Delivery price.

Anchor to [object Object]SubscriptionContractCalculationPaymentMethodInput

Payment methods use a @oneOf input pattern. Provide exactly one of the following options:

  • customerPaymentMethod: A vaulted customer payment method. Takes an id field (ID) for the customer payment method.
  • none: Explicitly set no payment method. Must be true.

Anchor to [object Object]SubscriptionContractCalculationFetchDeliveryOptionsInput

Use this input under deliveryMethod.fetchAvailableDeliveryOptions to fetch available delivery options without setting a delivery method.

  • address: The delivery address to fetch options for.
  • withDeliveryCustomizations: Optional Boolean. When true (the default), delivery customization functions run while fetching options. When false, delivery customization functions are bypassed and the raw options are returned.

Anchor to Handle asynchronous resultsHandle asynchronous results

The SubscriptionContractCalculation API uses asynchronous processing because it aligns contract editing with all other checkout surfaces across Shopify, including subscription billing attempts, checkout, and draft orders.

Most contract calculations complete in less than one second, but the system needs to account for network latency and errors in third-party services. The asynchronous model handles these situations without exposing transient errors or imposing strict API request timeouts.

You can handle contract calculation results by polling the contract calculation query or subscribing to webhooks for event-driven processing.

Anchor to Contract calculation statesContract calculation states

A contract calculation progresses through several states from creation to completion. Understanding these states helps you build integrations that handle all possible outcomes.

StateDescriptionWebhook
InitiatedThe contract calculation is created and waiting to be processed.—
ProcessingThe contract calculation is running with functions and external services.—
SucceededThe contract calculation is ready for review and commit.subscription_contract_calculations/succeed
FailedThe contract calculation failed. Errors are available on the result.subscription_contract_calculations/fail
VoidedThe contract calculation wasn't processed due to infrastructure issues.—
CommittedThe contract calculation has been committed and is now active.subscription_contracts/create or subscription_contracts/update

The subscriptionContractCalculation query returns a union with three possible GraphQL types. The internal states map to these types as follows:

GraphQL typeInternal states
SubscriptionContractCalculationPendingInitiated, Processing
SubscriptionContractCalculationSuccessSucceeded, Committed
SubscriptionContractCalculationFailureFailed, Voided
Uncommitted calculation retention

Shopify automatically deletes uncommitted calculations seven days after they're created. The subscriptionContractCalculation query returns null for a deleted calculation, and committing it returns a CALCULATION_NOT_FOUND error.

After creating a contract calculation, poll the subscriptionContractCalculation query until processing completes.

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

GraphQL query

query PollCalculation($id: ID!) {
subscriptionContractCalculation(id: $id) {
__typename
... on SubscriptionContractCalculationPending {
id
}
... on SubscriptionContractCalculationSuccess {
id
}
... on SubscriptionContractCalculationFailure {
id
}
}
}

  1. Initial wait: Wait 1 second after the mutation before the first poll.
  2. Poll interval: Poll every 500ms.
  3. Timeout: Stop polling after 30 seconds, or when the contract calculation result is in a failed state.

All contract calculation operations are safe to retry:

  • Calculate: Safe to retry. Creates a new contract calculation each time.
  • Commit: Safe to retry. If already committed, it returns success.
  • Polling: Safe to call as many times as needed.

Subscribe to webhooks for event-driven processing instead of polling.

Webhook topicDescription
subscription_contract_calculations/succeedFires when a contract calculation succeeds and the contract calculation is ready to commit.
subscription_contract_calculations/failFires when a contract calculation fails and errors are available on the result.

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

Create webhook subscription

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

JSON response

{
"data": {
"webhookSubscriptionCreate": {
"webhookSubscription": {
"id": "gid://shopify/WebhookSubscription/123456789"
},
"userErrors": []
}
}
}

Webhook payload

{
"id": 123,
"admin_graphql_api_id": "gid://shopify/SubscriptionContractCalculation/123",
"state": "succeeded"
}

Anchor to Backwards compatibilityBackwards compatibility

The SubscriptionDraft API will remain available but will not support the new capabilities of the SubscriptionContractCalculation API.

Anchor to Switch between the draft and calculation APIsSwitch between the draft and calculation APIs

You can use both the SubscriptionDraft API and the SubscriptionContractCalculation API on the same contracts, but only one API per editing session. The two APIs maintain their own pending state, and uncommitted drafts and calculation outputs aren't shared between them. A draft you create with one API isn't visible to the other, and a calculation output isn't visible to the draft API.

To switch from one API to the other between sessions:

  1. Abandon any pending draft or uncommitted calculation.
  2. Refetch the latest SubscriptionContract.
  3. Resubmit your complete remaining intent against the latest contract version using the API you're switching to.

Was this page helpful?