This guide is for developers building against Shopify's Subscription Contract API — whether you're shipping a public app, writing a custom integration for a single brand, or evaluating whether to migrate off a legacy subscription platform. Shopify replaced its original Subscriptions API in 2021 with a redesigned, GraphQL-only Subscription Contract API, and the new surface is significantly cleaner: contracts are first-class resources, drafts let you mutate atomically, selling plans are the primitive every product attaches to, and billing attempts are an explicit, idempotent action. The official docs cover the reference well but skip a lot of the operational knowledge — when to use a draft vs a direct update, why your billing attempts might silently succeed but produce no order, how the legacy v1 API's vestiges still leak through, what the rate limits actually are when you're billing thousands of contracts at once. This guide fills in those gaps. Everything below is accurate to the public API as of mid-2026 (the 2026-01 API version), though specific field names occasionally shift between versions — always check the current docs for canonical signatures before shipping.
Why this API exists (and what it replaced)
Shopify's original subscription support was a thin layer of webhooks and metafields layered on Stripe Subscriptions — apps maintained the canonical schedule in their own database, billed customers through a separate Stripe account, and synced orders back into Shopify after the fact. That model produced reliability problems (orders missing from Shopify's reporting, inventory not decrementing, refund flows that didn't agree between systems) and platform problems (customers being billed by an app, not by Shopify, which violated the merchant-of-record assumption Shopify Payments depended on).
In 2021 Shopify launched the Subscription Contract API: a redesign where Shopify itself is the merchant of record for every subscription charge, contracts are first-class records inside Shopify's database, the card is vaulted by Shopify Payments (not by the app), and billing is triggered through an explicit SubscriptionBillingAttempt mutation. The legacy Subscriptions API was deprecated and is no longer accepted for new apps — every subscription app on the App Store today builds on the Contract API. If you find tutorials or third-party blog posts referencing 'Shopify Subscriptions API' without 'Contract,' check the date: pre-2021 content is describing the deprecated surface and almost none of it is accurate anymore.
The naming overlap is unfortunate. 'Shopify Subscriptions API' historically referred to the legacy v1 surface (Stripe-backed, deprecated). 'Shopify Subscription Contract API' is the current platform. Any tutorial older than 2021 — or any newer tutorial that omits 'Contract' — is almost certainly describing the deprecated API. Cross-reference everything against the current GraphQL Admin API docs.
The core resources you'll model against
There are six resources every subscription app touches. Internalising what each one represents — and what it doesn't — saves a lot of confusion later.
- SellingPlanGroup — a collection of selling plans attached to one or more products. 'Subscribe & Save monthly' is typically a group containing a single monthly plan, but groups can hold multiple plans (e.g. weekly + biweekly + monthly under one 'Coffee subscription' group).
- SellingPlan — a single buyable subscription option: an interval, a discount, a delivery rule. Customers pick a SellingPlan when they subscribe; the contract records which plan was chosen.
- SubscriptionContract — the live agreement between merchant and customer. Holds status, customer reference, payment method, billing policy, delivery policy, pricing policy, lines, and the next billing date.
- SubscriptionLine — one product line on a contract. A contract typically has 1-5 lines (the products in the recurring order). Each line has its own price, quantity, variant, and reference back to the originating selling plan.
- SubscriptionDraft — a mutable, in-progress copy of a contract. You create a draft from a live contract, mutate it freely (add lines, change address, swap variants), then commit it back to replace the live contract atomically. This is the only safe way to make multi-field edits.
- SubscriptionBillingAttempt — the record of one attempt to charge a contract. Has a contract reference, an idempotency key, a status (ready / failed / completed), an error code on failure, and an order reference on success.
Selling plans are configured by the merchant (or by your app on their behalf) ahead of time and are reused across products. Contracts are created at customer signup. Drafts are short-lived. Billing attempts accumulate as a contract ages — a 2-year-old monthly contract has 24+ billing attempts in its history, which is useful audit data when investigating a dispute.
The shape of a SubscriptionContract
Most of your reads against the API are some flavour of 'load this contract with the fields I need.' The contract object is large but cleanly structured. Here's the canonical query shape your app will reuse constantly:
query GetContract($id: ID!) {
subscriptionContract(id: $id) {
id
status
nextBillingDate
currencyCode
customer { id email }
customerPaymentMethod { id }
billingPolicy { interval intervalCount }
deliveryPolicy { interval intervalCount }
deliveryPrice { amount currencyCode }
deliveryMethod { ... on SubscriptionDeliveryMethodShipping { address { address1 city zip countryCode } } }
lines(first: 50) {
edges { node { id title quantity currentPrice { amount currencyCode } variantId sellingPlanId } }
}
}
}Status transitions through ACTIVE → PAUSED → CANCELLED → EXPIRED, with FAILED as a holding state when billing attempts are failing. Billing policy and delivery policy are distinct: a 'pay every 3 months, ship every month' subscription is perfectly valid (used for prepaid quarterly plans). Lines are the products — each one carries its own selling plan reference, which matters because a contract can theoretically span multiple selling plans if your widget supports mixed-cart subscriptions.
Two important things missing from the contract: the credit card number (only the payment method ID is exposed), and the total amount of the next charge. Total is calculated at billing-attempt time from the lines + tax + shipping, so it's never stored on the contract. If you need to display 'your next charge will be $X' in your portal, you have to recompute it the same way Shopify will — sum the line prices, apply selling-plan pricing policy, add delivery price, apply tax estimate. Most apps approximate and accept a few cents of drift between portal display and final charge.
Selling plans and their three policies
A SellingPlan packages three policies: billingPolicy (when to charge), deliveryPolicy (when to ship), and pricingPolicy (what discount, if any, to apply). Each is independent. The common case — bill and deliver on the same interval, 10% off — looks like a single object but is technically three policies pointing at the same cadence with a fixed-percentage pricing rule.
Where it gets interesting is prepaid and multi-shipment plans. Prepaid: billingPolicy interval = 3 months, deliveryPolicy interval = 1 month — customer pays for 3 months upfront, gets 3 monthly shipments, then is billed again. Multi-shipment: same pattern, useful for box subscriptions where one charge funds multiple deliveries. Shopify supports both via the same selling plan shape — your app just configures the policies independently.
Pricing policy supports two modes: FIXED_AMOUNT (subtract a fixed dollar amount per cycle) and PERCENTAGE (subtract a percentage). It also supports adjustmentValue overrides per cycle, which is how 'first order 20% off, subsequent orders 10% off' is implemented — a fixed schedule of pricing policies keyed to cycle index. This is more flexible than most subscription tools and worth knowing if you're modelling promotional first-month deals. Deep dive on selling plans here.
Don't create a separate selling plan per product. Create one selling plan per cadence-and-discount combination (e.g. 'monthly 10% off'), then attach it to many products via the productVariants field on the SellingPlanGroup. This keeps the merchant's selling plan list manageable and makes 'change the discount for all monthly subscriptions' a single mutation instead of dozens.
SubscriptionDraft: the only safe way to mutate a contract
The most common mistake new integrators make is trying to update a live contract's lines, address, and pricing in three separate mutations. This race-conditions horribly: a cron tick can fire a billing attempt between mutation 1 and mutation 2, capturing the contract in a half-edited state. The fix is the draft pattern, which Shopify designed specifically to prevent this.
A draft is created from a live contract via subscriptionContractUpdate (which somewhat confusingly returns a draft, not a finished update). You then mutate the draft as many times as you want using draft-scoped mutations (subscriptionDraftLineUpdate, subscriptionDraftLineAdd, subscriptionDraftCommit, etc.). When the customer-portal save button is clicked, you call subscriptionDraftCommit, and Shopify atomically replaces the live contract with the draft's state. No half-edited contracts. No race with the billing cron.
Some operations bypass drafts because they're inherently atomic — pausing or cancelling a contract is a single status flip and has its own dedicated mutations (subscriptionContractPause, subscriptionContractCancel). Single-field updates like 'set next billing date' have a dedicated mutation too (subscriptionContractSetNextBillingDate). The draft pattern is specifically for multi-field edits and line-level changes.
A draft that's never committed is a leaked resource. Shopify garbage-collects abandoned drafts but the rules are version-dependent and not well-documented. Build your portal so that 'save changes' always commits or 'cancel changes' always calls subscriptionDraftDiscard. Don't leave drafts hanging.
SubscriptionBillingAttempt: how renewals actually fire
Renewals happen because your app fires a SubscriptionBillingAttemptCreate mutation against a contract. Shopify does not have a built-in cron that bills contracts — scheduling is the app's responsibility. Most apps run a worker every 15 minutes that queries for contracts where nextBillingDate is in the past and ACTIVE, then fires one billing attempt per contract.
mutation BillContract($contractId: ID!, $idempotencyKey: String!) {
subscriptionBillingAttemptCreate(
subscriptionContractId: $contractId,
subscriptionBillingAttemptInput: {
idempotencyKey: $idempotencyKey,
originTime: null
}
) {
subscriptionBillingAttempt {
id
ready
errorCode
errorMessage
nextActionUrl
order { id name }
}
userErrors { field message }
}
}The idempotencyKey is required. Make it deterministic — a hash of contract ID + scheduled billing date is the conventional choice. If you submit the same key twice, Shopify returns the same billing attempt rather than creating a new one, which is your protection against accidental double-charges when your worker crashes mid-flight and retries.
The response field ready: false indicates Shopify accepted the attempt but hasn't completed it yet — billing attempts are async. The subscription_billing_attempts/success or /failure webhook is your authoritative signal that the attempt resolved. nextActionUrl appears for SCA challenges that need customer interaction. errorCode appears on synchronous validation failures (e.g., trying to bill a CANCELLED contract).
Webhooks: keeping your app in sync
Webhooks are how your app finds out about lifecycle events without polling. The set worth subscribing to is small. Subscribe via the webhookSubscriptionCreate mutation, ideally during app installation, and re-subscribe after any scope change.
- SUBSCRIPTION_CONTRACTS_CREATE — a new contract was created (first subscription checkout, or via API import during migration)
- SUBSCRIPTION_CONTRACTS_UPDATE — any field on a contract changed (address, lines, status, next billing date, payment method)
- SUBSCRIPTION_BILLING_ATTEMPTS_SUCCESS — a billing attempt resolved successfully and an order was created
- SUBSCRIPTION_BILLING_ATTEMPTS_FAILURE — a billing attempt failed; payload includes
errorCodefor branching dunning logic - ORDERS_CREATE — the renewal order itself, useful for fulfillment-side handlers and reporting integrations
- CUSTOMERS_UPDATE — customer record changed, useful if you maintain a denormalised customer view
Two operational notes. First, webhooks can arrive out of order — design handlers as idempotent upserts keyed by the Shopify ID, never assume create-before-update. Second, Shopify retries failed webhook deliveries for up to 48 hours with exponential backoff; if your endpoint returns a 5xx, you'll see the event again, possibly several times. Return 2xx within 5 seconds (push real work to a background queue) or you'll be replaying events all afternoon.
App Bridge vs raw GraphQL: when to use which
Most subscription apps embed an admin UI inside Shopify using App Bridge — Shopify's iframe-aware UI framework. App Bridge gives you session-token authentication, host-aware navigation, Polaris web components for native-feeling UI, and direct access to the Admin GraphQL API from the embedded app. For merchant-facing screens, this is the right default — you get authentication, theming, and embedding for free.
Raw GraphQL (server-to-server, with an offline access token from OAuth) is the right choice for: cron-triggered billing, webhook processing, batch operations, anything that doesn't have a logged-in merchant in front of it. The OAuth offline token, scoped narrowly to what your app needs, is what your cron worker uses to authenticate. You'll fire SubscriptionBillingAttemptCreate from a server worker using this token, not from the merchant's browser via App Bridge.
A few hybrid patterns are worth mentioning. The customer portal (where subscribers manage their own contracts) typically lives on a public storefront-adjacent domain — neither admin App Bridge nor server cron, but a third surface that authenticates via Shopify Customer Account API tokens. The Subscription Contract API can be called with these customer tokens via the storefront-adjacent endpoints, with permissions automatically scoped to the customer's own contracts. This is how SimpleSubscription's customer portal avoids exposing admin scopes to the subscriber-facing surface.
OAuth scopes and Protected Customer Data
Subscription apps need a specific bundle of scopes that's larger than most Shopify apps. Get this right at install time — adding scopes later forces a re-authorisation prompt that some merchants ignore for weeks.
read_own_subscription_contracts,write_own_subscription_contracts— required for any contract operationsread_customers,write_customers— required to associate contracts with customer records and update addressesread_orders,write_orders— required to read renewal orders and handle refunds/fulfillmentread_products,write_products— required to read subscribable products and attach selling plansread_purchase_options,write_purchase_options— required to create and manage selling plan groupsread_customer_payment_methods— required to read the payment method handle a contract bills againstread_discounts,write_discounts— required if you support promotional discounts at signup
On top of OAuth scopes, Shopify applies Protected Customer Data rules to any app that reads PII. Subscription apps invariably do (customer email, name, address are all PCD). You'll go through Shopify's protected customer data review during App Store submission, which requires: a privacy policy, opt-in disclosure for what PCD you collect, documented retention periods, and a data-deletion flow (GDPR-style). Plan for 2-4 weeks for this review on first submission.
Apps that request scopes they don't actually use fail Shopify's app review. Audit your token usage before submission — if you have write_themes in your manifest but never write to a theme, remove it. The reviewer will check.
API gotchas that will trip you up
A non-exhaustive list of things that don't appear in the reference docs but will eat days of your time if you don't know about them upfront.
- Partial line updates are awkward — there's no 'update line X's quantity from 2 to 3' direct mutation, you go through the draft pattern, mutate the line, commit. This is fine but feels heavier than it should for a one-field change.
- Status transitions are gated — you can't reactivate a CANCELLED contract; you have to create a new one. PAUSED → ACTIVE is supported. Plan your UI around this constraint.
- Gift card eligibility for subscriptions is limited — gift cards can't pay for subscription renewals because they can't be vaulted. Your widget should disallow gift-card-only checkouts when a selling plan is in cart.
- Rate limits apply to the GraphQL Admin API: the bucket is 1000 cost-points per app per shop, refilling at 50 points/second.
subscriptionBillingAttemptCreatecosts more than a typical query — pace your billing batches accordingly. At very high volume, contact Shopify Partner Support about higher limits. - Idempotency keys are scoped per contract — the same key on different contracts is fine. The key collision check is contract-local.
- Time zones on
nextBillingDateare stored in UTC but displayed in merchant local time in the admin — your cron logic should always compare in UTC to avoid off-by-one-day bugs around DST transitions.
Testing patterns: dev stores, bogus gateway, and dry-run billing
Shopify provides a bogus payment gateway for development stores that simulates real payment behaviour without moving actual money. Card number 1 succeeds, 2 fails generically, 3 fails with insufficient funds — useful for testing dunning flows. Enable it under Settings → Payments → 'Manual payment methods' on a dev store, and treat it as your default for integration tests.
For end-to-end billing tests, the conventional pattern is: create a development store, install your app, create a real subscription contract via your widget, manually advance the contract's nextBillingDate using subscriptionContractSetNextBillingDate to one minute in the future, let your cron fire the billing attempt, and observe the webhooks. This exercises the full pipeline without waiting 30 days. Always clear bogus gateway charges by deleting the dev store at the end of a test session — they don't appear in any payout but accumulate in admin reporting.
For load testing the billing cron, generate hundreds of contracts in a dev store and advance them all to the same near-future date, then watch how your worker behaves. The most common bug at this layer is concurrent billing attempts on the same contract from two worker instances — a properly-keyed idempotency strategy makes this safe, but a sloppy one will double-charge. Test it before production.
Developer FAQs
Is the legacy Shopify Subscriptions API still supported?
No — the original v1 Subscriptions API was deprecated in 2021 when the Subscription Contract API launched and is no longer accepted for new apps. Existing apps that haven't migrated have been steadily losing functionality. If you're building anything new in 2026, target the Subscription Contract API exclusively.
Do I need Shopify Plus to use the Subscription Contract API?
No. The API is available on all Shopify plans. Shopify Plus has higher rate limits and some additional checkout customization, but the subscription primitives themselves are the same across plans. The only platform-level requirement is a supported payment gateway with stored payment method capability (Shopify Payments in most markets).
What's the rate limit for SubscriptionBillingAttemptCreate?
The mutation is bound by the standard Admin API rate limit: 1000 cost-points per app-shop pair, refilling at 50 points/second. Billing attempts cost ~10 points each in practice. A naïve worker can fire 5 attempts per second sustained, with burst headroom for ~100 attempts. At higher volume (10k+ contracts billing on the same day), batch with backoff, or contact Shopify Partner Support about enterprise limits.
Can I customize the order created from a billing attempt?
Mostly no. The renewal order is created by Shopify based on the contract's lines and policies. You can add tags to subscription orders via the API after creation, attach metafields, modify line items via order editing, and apply post-creation discounts, but you can't intercept the order before it's created. If you need pre-order customization (gift wrap, custom message), it has to live on the contract itself, not be added at billing time.
What scopes are required for a subscription app?
At minimum: read_own_subscription_contracts, write_own_subscription_contracts, read_customers, write_customers, read_orders, write_orders, read_products, write_products, read_purchase_options, write_purchase_options, read_customer_payment_methods. Most apps also need read_discounts/write_discounts and read_themes/write_themes (for theme app extension installation). Don't request scopes you don't use — Shopify's App Review will flag it.
What's the difference between updating a contract directly and using a draft?
Some single-field mutations operate directly on the contract (subscriptionContractPause, subscriptionContractCancel, subscriptionContractSetNextBillingDate) — these are atomic by nature. Multi-field changes (adding/removing lines, changing variants, changing prices, changing addresses together) must go through a SubscriptionDraft, which is mutated in place and then committed atomically. Trying to do multi-field changes with sequential direct updates creates race conditions with the billing cron.
Can a subscription contract have multiple selling plans?
Yes — each subscription line on a contract references its own selling plan. In theory a contract can span multiple selling plans (e.g. monthly coffee + quarterly tea on the same contract). In practice most apps' widgets create one contract per selling plan because it simplifies the customer-facing model. Both patterns are valid API-wise.
How do I migrate contracts from another subscription platform?
Via SubscriptionContract creation directly through the API. The migration mutation lets you specify all contract fields (status, next billing date, lines, address, billing/delivery policies) and links to a customer payment method that was previously imported. The catch is the payment method itself — to import vaulted cards from Recharge, Loop, or Skio, you go through Shopify's Customer Payment Method import endpoint, which requires the previous gateway to participate in a card-handoff process. Most migrations from Stripe-backed apps work cleanly because Shopify Payments is Stripe Connect underneath. See our <a href="/subscription-migration">migration guide</a>.
Do I need to handle 3D Secure / SCA manually?
For customer-present transactions (signup, payment method update), Shopify's checkout handles SCA natively. For recurring charges, most renewals qualify for the merchant-initiated transaction exemption from SCA in the EU/UK. When an issuer challenges a renewal anyway, the billing attempt returns AUTHENTICATION_ERROR with a nextActionUrl that your app can email to the customer. The customer follows the link, completes the challenge in a Shopify-hosted flow, and your app retries the billing attempt.
How do I test the API without a live merchant?
Create a Shopify development store via the Partners Dashboard (free, unlimited), install your app on it, and use Shopify's bogus payment gateway for billing tests. Card number '1' succeeds, '2' fails generically, '3' fails with insufficient funds. Combined with subscriptionContractSetNextBillingDate to manually advance contracts, you can exercise the full lifecycle in minutes without touching production.
What's the right authentication for a customer-facing portal?
Use Shopify Customer Account API tokens, not admin session tokens. Customer tokens scope contract access automatically to the logged-in customer's own contracts — your portal can call subscriptionContract queries with a customer token and Shopify enforces the customer-owns-this-contract check server-side. This is the safest pattern; never expose admin scopes to a customer-facing surface.
Does Shopify provide an SDK for the Subscription Contract API?
There's no dedicated 'subscriptions SDK' — it's just GraphQL under the standard Admin API surface, so the official @shopify/shopify-api Node library and the equivalents in Ruby/PHP/Python all work directly. App Bridge handles authentication and host context for the embedded admin UI; the underlying API calls are vanilla GraphQL.