Shopify App Billing API: When You Still Need It

The Shopify app billing API (appSubscriptionCreate, appUsageRecordCreate, appPurchaseOneTimeCreate) still works, but as of October 2026 Shopify calls it legacy. New public apps are expected to use Shopify App Pricing, where plans live in the Partner Dashboard and Shopify hosts the plan page. You only need the Billing API for one-time purchases, usage caps, per-shop custom pricing, or a pricing model App Pricing does not support yet.

That is the decision. The rest of this post is what each route looks like in code, written against Admin GraphQL 2026-10, so you can pick one before you build a pricing page you may not need.

Which should a new app use: App Pricing or the Billing API?

The billing overview now lists two methods. Shopify App Pricing is “the default way to charge merchants” for new public apps. The Billing API is filed under manual pricing, described as being for “existing integrations using billing.request or appSubscriptionCreate”. The changelog entry of 12 May 2026 is blunter: “The Billing API continues to function but is now legacy. All apps should use Shopify App Pricing going forward.”

App Pricing was called Managed Pricing until that rename, and the rename brought usage billing with it. You define meters in the Partner Dashboard, send events through the App Events API, and Shopify aggregates and invoices. Before May 2026 Managed Pricing covered recurring plans only, so any metered component meant the Billing API.

The App Pricing docs list what it still cannot do: no one-time purchases, no usage caps, no custom pricing per shop, at most eight public plans, usage billed monthly only. Those gaps are the Billing API’s remaining territory.

One constraint cuts the other way. Once you opt an app into App Pricing, “you can’t create new recurring application charges using the Billing API.” Existing charges keep processing. So the choice is per app, not per merchant, and you cannot run both for new subscriptions.

If you are building a public app with monthly plans and maybe a metered component, stop here and use App Pricing. The merchant picks a plan at https://admin.shopify.com/store/:store_handle/charges/:app_handle/pricing_plans, Shopify redirects back to your configured URL with plan_handle and shop appended, and you never write a checkout flow.

How do you create a subscription with appSubscriptionCreate?

The mutation takes a name, one or more lineItems, a returnUrl, and optionally test, trialDays and replacementBehavior. Each line item carries exactly one of appRecurringPricingDetails or appUsagePricingDetails. Recurring pricing is a price { amount currencyCode } plus an interval of EVERY_30_DAYS or ANNUAL, with an optional discount. Usage pricing is a terms string shown to the merchant and a cappedAmount.

The response gives you confirmationUrl. Nothing is billed until the merchant opens that URL and approves. Your job is to redirect them there and handle the return.

// Admin GraphQL 2026-10, run with a shop-scoped access token
const CREATE_SUBSCRIPTION = `#graphql
  mutation CreateSubscription($returnUrl: URL!, $test: Boolean!) {
    appSubscriptionCreate(
      name: "Pro"
      returnUrl: $returnUrl
      test: $test
      trialDays: 14
      lineItems: [
        {
          plan: {
            appRecurringPricingDetails: {
              price: { amount: 29.0, currencyCode: USD }
              interval: EVERY_30_DAYS
            }
          }
        }
        {
          plan: {
            appUsagePricingDetails: {
              terms: "0.05 USD per order synced"
              cappedAmount: { amount: 100.0, currencyCode: USD }
            }
          }
        }
      ]
    ) {
      confirmationUrl
      appSubscription { id }
      userErrors { field message }
    }
  }
`;

Two line items, one recurring and one usage, is the hybrid shape: a base fee plus a metered overage, with a cap the merchant agreed to when they approved.

If you are on the Remix template, @shopify/shopify-app-remix wraps all of this. Plans go in the billing block of shopifyApp(), each as lineItems of amount, currencyCode and an interval from BillingInterval (Every30Days, Annual, OneTime, Usage), with optional trialDays. Then a loader guards itself:

import { BillingInterval } from "@shopify/shopify-app-remix/server";

export const PRO_PLAN = "Pro";

// in shopifyApp({ ... })
billing: {
  [PRO_PLAN]: {
    trialDays: 14,
    lineItems: [
      { amount: 29, currencyCode: "USD", interval: BillingInterval.Every30Days },
    ],
  },
},

// in a route loader
export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { billing } = await authenticate.admin(request);
  const isTest = process.env.NODE_ENV !== "production";
  await billing.require({
    plans: [PRO_PLAN],
    isTest,
    onFailure: async () => billing.request({ plan: PRO_PLAN, isTest }),
  });
  return null;
};

billing.require throws if there is no active payment, and onFailure decides what to do about it. billing.request returns Promise<never> because it redirects the merchant to the confirmation page and never comes back. The Remix billing reference covers the rest: billing.check for a non-throwing status read, billing.cancel with prorate, and billing.createUsageRecord.

How do you check the merchant is actually paying?

Outside the Remix helpers, the source of truth is the currentAppInstallation query. It needs no arguments and returns the AppInstallation for the calling app, with activeSubscriptions (a plain list of AppSubscription), allSubscriptions (a connection, including cancelled and declined ones) and oneTimePurchases.

Check activeSubscriptions on every request that unlocks paid behaviour, not once at install. Merchants cancel from the admin and trials lapse, so a cached “paid” flag in your session table drifts away from what Shopify will actually invoice.

The same applies to returnUrl. The docs describe it as where the merchant lands after approving, but it is an ordinary URL in your app that anyone can open directly. Query the installation when the merchant arrives there rather than treating the arrival as payment.

How do usage charges work on each route?

On the Billing API, you charge with appUsageRecordCreate. It takes the subscriptionLineItemId of the usage line item, a price, a description, and an optional idempotencyKey of up to 255 characters. Use the key. Your worker will retry, and without it you double-charge.

The cap is enforced server-side. If a record would push the interval’s total past cappedAmount, the mutation fails with the user error “Failed to create usage charge”. That is a signal to stop doing the billable work, or to ask the merchant to raise their cap, not a bug to retry.

Under App Pricing the mechanics are different. You do not create charges at all. You send events to the App Events API: a POST to https://api.shopify.com/app/unstable/events with a client-credentials bearer token, carrying shop_id, an event_handle, an ISO 8601 timestamp, an idempotency_key and free-form attributes. If the handle matches a meter you defined in the Partner Dashboard, it counts as usage. As of October 2026 that endpoint is on the unstable path, so pin a date in your code comment and expect it to move.

Note what you lose by going this way: there is no cap. The merchant agrees to tiered rates, not a ceiling. If your product needs a hard spend limit per shop, that alone puts you back on the Billing API.

What about test charges and trials?

Every create mutation takes test: Boolean, default false. A test subscription goes through the full approval flow without billing anything. Drive it from configuration rather than a literal in the mutation, because a test: true that reaches production charges nobody, and you will not notice until the first invoice run comes up empty.

trialDays counts from the day the merchant approves, not from install, so a merchant who installs and ignores the pricing page for a week has not consumed any trial. Shopify’s own best-practice note on the billing overview is to offer a trial and to keep the number of plans small.

For one-off charges, appPurchaseOneTimeCreate takes name, price, returnUrl and test and returns the same confirmationUrl shape. App Pricing has no equivalent, which is why a setup fee or a paid migration is still a Billing API job.

App subscriptions are also a different thing from the product subscriptions merchants sell to their customers. Those go through selling plans and contracts, which our post on the Shopify Selling Plans API covers, and the two never share a mutation.

What I would do

For a new public app with recurring plans, use Shopify App Pricing and do not write a billing page. Still read the plan the merchant is on, from the plan_handle Shopify appends on return or from activeSubscription on the Partner API, because which features to unlock is your code’s problem either way.

Reach for the Billing API when you have a documented reason: a one-time charge, a usage cap the merchant can see, a bespoke price for one shop, or an existing app with live charges that you are not ready to migrate. Write the appSubscriptionCreate call with test driven by configuration, store the subscriptionLineItemId you get back, and send every usage record with an idempotency key.

Do not start a brand-new app on the Billing API just because the sample code is older and more plentiful. Shopify has already labelled it legacy, and a billing page you wrote yourself is one more thing to migrate later.

Whoooop builds Shopify apps and the server-side plumbing around them, including session handling, billing and the webhooks that keep both honest; our post on verifying Shopify session tokens in Node is the companion to this one. If you are scoping an app and want the billing decision made before the first commit, our Shopify development page explains how we work.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch