ADSX
JULY 21, 2026

Shopify App Billing API: Subscriptions and Usage (2026)

Implement Shopify app billing with GraphQL: appSubscriptionCreate, usage records, free trials, test charges, and proration, with working mutations.

AUTHOR
AT
AdsX Team
E-COMMERCE SPECIALISTS
READ TIME
11 MIN
SUMMARY

Implement Shopify app billing with GraphQL: appSubscriptionCreate, usage records, free trials, test charges, and proration, with working mutations.

The Shopify App Billing API is a set of GraphQL Admin API mutations that charge merchants through their Shopify invoice: appSubscriptionCreate for recurring plans, appUsageRecordCreate for metered billing, and appPurchaseOneTimeCreate for one-off charges. Shopify collects the money, and you keep 100% of your first $1M in lifetime app revenue before a 15% share applies.

Calculator and financial documents representing app subscription billing
CALCULATOR AND FINANCIAL DOCUMENTS REPRESENTING APP SUBSCRIPTION BILLING

Apps distributed through the Shopify App Store must use Shopify's billing system, so this is not an optional integration. The good news: because charges land on the merchant's existing Shopify invoice, conversion to paid is meaningfully better than asking merchants to enter a credit card in a third-party checkout. This guide covers the GraphQL mutations, trials, usage caps, proration, test charges, and the bugs that bite most billing implementations. It assumes you have an app running with session token auth; if not, start with the Shopify app development guide.

All examples use the GraphQL Admin API (version 2026-04 at the time of writing). REST billing endpoints are legacy and should not be used for new work; the GraphQL vs REST comparison covers why in detail.

Managed pricing vs the Billing API

Before writing any billing code, decide whether you need to. Shopify now offers two paths:

Managed pricing (Shopify App Pricing)Billing API (manual)
Where plans are definedPartner DashboardYour code, at runtime
Plan selection pageHosted by ShopifyBuilt by you
Upgrades and downgradesHandled by Shopify, proratedYou call mutations and pick replacement behavior
Free trialsConfigured per plan, trial days tracked across reinstallstrialDays argument, reinstall tracking is on you
Usage-based chargesSupported via usage-based plan componentsFull control with appUsageRecordCreate
Per-merchant custom pricingNoYes
Billing code you maintainRoughly zeroMutations, webhooks, state machine

Shopify's own billing documentation now positions managed pricing as the recommended path and manual Billing API pricing as the option for apps with specific needs. That framing is fair. If your pricing is a simple grid of monthly plans, managed pricing removes an entire class of bugs, including the reinstall-trial exploit and the returnUrl race described later.

Use the Billing API when you need things managed pricing cannot do: negotiated per-merchant prices, dynamic pricing based on store size, usage charges computed by your own logic, contextual upgrade prompts inside your app, or migration flows from grandfathered plans. Most apps earning serious revenue end up here eventually. Worth noting: Mantle, a popular third-party billing layer, is shutting down, which has pushed many partners back to first-party billing code, and this guide covers what that code needs to handle.

How a billing flow actually works

Every charge type follows the same three-step dance:

  1. Your app calls a create mutation. Shopify returns a confirmationUrl.
  2. You redirect the merchant to confirmationUrl, a Shopify-hosted approval page.
  3. The merchant approves. Shopify activates the charge and redirects them to your returnUrl.

The subscription is not active until the merchant approves. Between steps 1 and 3 it sits in PENDING, and merchants abandon approval pages more often than you would expect. Treat activation as an event you observe (via the APP_SUBSCRIPTIONS_UPDATE webhook or a status check on next load), never as something you assume because you sent someone to the approval page.

Creating a recurring subscription

Here is the core mutation for a monthly plan:

mutation CreateSubscription(
  $name: String!
  $lineItems: [AppSubscriptionLineItemInput!]!
  $returnUrl: URL!
  $trialDays: Int
  $test: Boolean
) {
  appSubscriptionCreate(
    name: $name
    lineItems: $lineItems
    returnUrl: $returnUrl
    trialDays: $trialDays
    test: $test
  ) {
    appSubscription {
      id
      status
    }
    confirmationUrl
    userErrors {
      field
      message
    }
  }
}

With variables for a $29/month plan and a 14-day trial:

{
  "name": "Growth Plan",
  "returnUrl": "https://yourapp.example.com/billing/confirm",
  "trialDays": 14,
  "test": false,
  "lineItems": [
    {
      "plan": {
        "appRecurringPricingDetails": {
          "price": { "amount": 29.0, "currencyCode": "USD" },
          "interval": "EVERY_30_DAYS"
        }
      }
    }
  ]
}

A few practical notes. The interval is EVERY_30_DAYS, not calendar-monthly; billing cycles are 30-day windows from activation. Switching interval to ANNUAL gives you yearly billing, which merchants increasingly expect as a discounted option and which cuts involuntary churn from failed monthly payments. And name shows up on the merchant's invoice, so make it recognizable; "Pro" from an app whose brand name appears nowhere generates support tickets and chargebacks of goodwill.

To check what a shop currently has, query the current installation:

query ActiveSubscription {
  currentAppInstallation {
    activeSubscriptions {
      id
      name
      status
      test
      trialDays
      currentPeriodEnd
    }
  }
}

Run this on app load rather than trusting your local database. Merchants cancel subscriptions by uninstalling, and Shopify cancels the subscription automatically at uninstall, so your database will drift from reality if a webhook gets missed.

Usage-based and metered billing

Usage billing attaches a second line item to the subscription. A subscription can hold at most one recurring line item and one usage line item, so the common "base fee plus per-order fee" model looks like this:

{
  "name": "Scale Plan",
  "returnUrl": "https://yourapp.example.com/billing/confirm",
  "lineItems": [
    {
      "plan": {
        "appRecurringPricingDetails": {
          "price": { "amount": 49.0, "currencyCode": "USD" },
          "interval": "EVERY_30_DAYS"
        }
      }
    },
    {
      "plan": {
        "appUsagePricingDetails": {
          "terms": "$0.05 per order processed",
          "cappedAmount": { "amount": 100.0, "currencyCode": "USD" }
        }
      }
    }
  ]
}

The terms string is shown to the merchant on approval; write it the way you would explain the fee to a customer. The cappedAmount is a hard ceiling on what you can bill in one 30-day cycle, and the merchant is agreeing to the cap, not to unlimited usage.

You then record charges as they happen:

mutation RecordUsage(
  $subscriptionLineItemId: ID!
  $price: MoneyInput!
  $description: String!
  $idempotencyKey: String
) {
  appUsageRecordCreate(
    subscriptionLineItemId: $subscriptionLineItemId
    price: $price
    description: $description
    idempotencyKey: $idempotencyKey
  ) {
    appUsageRecord {
      id
    }
    userErrors {
      field
      message
    }
  }
}

Two failure modes to design for. First, a usage record that would push the cycle total past cappedAmount is rejected with a userError, and your app silently stops earning while continuing to deliver service. Track the line item's balanceUsed against the cap and prompt the merchant to approve a raise (via appSubscriptionLineItemUpdate, which returns its own confirmationUrl) at around 80% consumption. Second, network retries can create duplicate charges; always pass an idempotencyKey (a UUID tied to the underlying event, like the order ID) so a replayed mutation is a no-op. Merchants notice double charges faster than any monitoring you will ever build.

If you batch usage, aggregate hourly or daily rather than posting one record per event. Usage record mutations count against your API rate limits like any other request, and a high-volume store can generate more billable events than your rate limit budget comfortably absorbs.

One-time charges

For lifetime deals, setup fees, or paid add-ons, appPurchaseOneTimeCreate follows the same confirm-and-return flow:

mutation OneTimePurchase($name: String!, $price: MoneyInput!, $returnUrl: URL!, $test: Boolean) {
  appPurchaseOneTimeCreate(name: $name, price: $price, returnUrl: $returnUrl, test: $test) {
    appPurchaseOneTime {
      id
      status
    }
    confirmationUrl
    userErrors {
      field
      message
    }
  }
}

One-time charges are not refundable through the API; refunds go through Partner support. Price accordingly, and prefer subscriptions for anything that resembles ongoing service. App Store review also looks skeptically at large one-time charges positioned before merchants can evaluate the app.

Test charges

Set test: true on any create mutation and the full flow runs, including approval, ACTIVE status, webhooks, and usage records, but no money moves. Test subscriptions are flagged with test: true on the subscription object so you can tell them apart in queries.

Develop against a development store: they never incur charges, they can install unreleased apps, and you get one free from any Partner account. You can also point a staging build at a regular Shopify trial store when you want to rehearse the merchant experience end to end on a store that looks like production.

Guard the flag with configuration, not code edits. A hardcoded test: true that ships to production is one of the classic billing bugs: everything works, dashboards look healthy, and revenue is zero.

Upgrades, downgrades, and proration

When a merchant changes plans, you do not mutate the existing subscription. You call appSubscriptionCreate again with the new plan, and the replacementBehavior argument controls what happens to the old one:

replacementBehaviorWhat happens
STANDARD (default)Cancels the current subscription immediately on approval and replaces it, except in a few cases (such as annual-to-annual downgrades and annual-to-monthly switches in the same currency) where the swap defers to the next cycle
APPLY_IMMEDIATELYAlways cancels and replaces the moment the merchant approves
APPLY_ON_NEXT_BILLING_CYCLEKeeps the current subscription until the cycle ends, then swaps

For immediate swaps, Shopify prorates: the merchant receives credit for the unused portion of the old charge against the new one, so an upgrade mid-cycle does not double-bill. The exact credit math lives in Shopify's subscription billing docs and is worth reading before you promise specific numbers in your pricing UI.

Practical guidance: use the default STANDARD for upgrades so merchants get the new features instantly, and APPLY_ON_NEXT_BILLING_CYCLE for downgrades so nobody feels penalized for paying ahead. Whatever you choose, subscribe to the APP_SUBSCRIPTIONS_UPDATE webhook topic and treat it as the source of truth for plan state. The webhook fires on activation, cancellation, and replacement, and it is how you find out about the cancellations that happen without your UI being involved.

What Shopify takes: revenue share in 2026

The economics, per Shopify's revenue share documentation:

  • 0% revenue share on your first $1,000,000 USD of lifetime gross App Store revenue, for developers who register (one-time $19 USD fee per Partner account).
  • 15% on everything above that lifetime threshold.
  • A 2.9% payment processing fee applies separately, plus applicable taxes.
  • Large developers, defined as $20M+ USD in prior-year App Store earnings or $100M+ USD gross company revenue, pay 15% on all app revenue with no 0% band.

The important change: since January 1, 2025 the $1M threshold is lifetime, not annual. Under the old terms the counter reset every January. Revenue is also aggregated across associated developer accounts, so splitting apps across Partner accounts does not restart the clock. Model your unit economics on 15% as the steady state and treat the 0% band as runway. Achieving the Built for Shopify badge does not change the revenue split, but it does change how much revenue there is to split.

Common billing bugs (and how to avoid them)

Trusting the returnUrl. Merchants close tabs. If you flip a shop to paid only when they land on your returnUrl, you will have paying customers stuck on free and free users you believe are paying. Reconcile against activeSubscriptions on every app load and consume the APP_SUBSCRIPTIONS_UPDATE webhook.

Free trial resets on reinstall. Uninstalling cancels the subscription; reinstalling and subscribing again grants another trialDays. Persist first-trial dates per shop domain on your side and reduce the trial for returning shops.

Usage silently capped. Once a cycle's usage hits cappedAmount, further records fail. Alert yourself, prompt the merchant, and decide deliberately whether service continues free or pauses.

Duplicate usage records. A timeout plus a retry without idempotencyKey equals a double charge and a one-star review.

Frozen and declined states. Subscriptions can leave ACTIVE when a merchant's payment fails or their store closes. Handle FROZEN, CANCELLED, and EXPIRED explicitly rather than treating everything that is not ACTIVE as new.

Test flags in production. Covered above; assert against it in CI.

Currency assumptions. Billing supports multiple currencies, but your reporting probably assumes one. Store the currencyCode with every charge from day one.

Next step

Spin up a development store, create your subscription flow with test: true, and walk it end to end four times: fresh install, upgrade, downgrade, and uninstall-then-reinstall. Log the APP_SUBSCRIPTIONS_UPDATE payload at every transition and confirm your database matches currentAppInstallation.activeSubscriptions after each pass. If those four paths reconcile cleanly, you have a billing implementation ahead of most of the App Store; the Admin API guide covers the auth and rate-limit fundamentals underneath it.

ABOUT THE AUTHOR
AT
AdsX Team
AI SEARCH SPECIALISTS

The AdsX team helps brands navigate AI-powered search and get recommended by ChatGPT, Claude, Perplexity, and other AI platforms. With deep expertise in LLM optimization, paid media, and e-commerce growth, our team has driven a 340% average increase in AI mentions for clients across industries.

MORE BY ADSX TEAM

Ready to Dominate AI Search?

Get your free AI visibility audit and see how your brand appears across ChatGPT, Claude, and more.

Get Your Free Audit