Skip to main content
This guide covers implementing billing for your Shopify app, including one-time charges and recurring subscriptions.

Overview

The library supports multiple billing models:
  • Recurring subscriptions - Monthly or annual recurring charges
  • Usage-based billing - Pay-as-you-go pricing
  • One-time charges - Single payment

Configuration

Define your billing plans in the config:
import {shopifyApi, BillingInterval} from '@shopify/shopify-api';

const shopify = shopifyApi({
  // ... other config
  billing: {
    'Basic Plan': {
      lineItems: [
        {
          interval: BillingInterval.Every30Days,
          amount: 10.00,
          currencyCode: 'USD',
        },
      ],
    },
    'Premium Plan': {
      lineItems: [
        {
          interval: BillingInterval.Annual,
          amount: 100.00,
          currencyCode: 'USD',
        },
      ],
    },
    'Usage Plan': {
      lineItems: [
        {
          interval: BillingInterval.Usage,
          amount: 1000.00,
          currencyCode: 'USD',
          terms: 'Up to $1,000 per month',
        },
      ],
    },
    'One-Time Charge': {
      interval: BillingInterval.OneTime,
      amount: 50.00,
      currencyCode: 'USD',
    },
  },
});

Checking for Active Subscriptions

Check if a shop has an active payment before allowing access:
app.get('/api/products', async (req, res) => {
  const session = await loadSession(req);
  
  const hasPayment = await shopify.billing.check({
    session,
    plans: ['Basic Plan', 'Premium Plan'],
    isTest: process.env.NODE_ENV !== 'production',
  });

  if (!hasPayment) {
    return res.status(403).json({
      error: 'No active subscription',
      redirectUrl: '/billing',
    });
  }

  // Continue with request
});
Parameters:
  • session - Current shop session
  • plans - Array of plan names to check (optional - checks all if omitted)
  • isTest - Whether to include test charges
Source: lib/billing/check.ts:40-67

Check Response

By default, check() returns a boolean. Use returnObject: true for detailed information:
const result = await shopify.billing.check({
  session,
  plans: ['Basic Plan'],
  isTest: true,
  returnObject: true,
});

console.log(result);
// {
//   hasActivePayment: true,
//   appSubscriptions: [{
//     id: 'gid://shopify/AppSubscription/1',
//     name: 'Basic Plan',
//     test: true,
//     lineItems: [...]
//   }],
//   oneTimePurchases: []
// }
Source: lib/billing/check.ts:69-109

Requesting Payment

Request payment when a shop needs to subscribe:
app.post('/api/billing/subscribe', async (req, res) => {
  const session = await loadSession(req);
  const {plan} = req.body;
  
  const confirmationUrl = await shopify.billing.request({
    session,
    plan: plan,
    isTest: process.env.NODE_ENV !== 'production',
  });

  res.json({confirmationUrl});
});
The confirmationUrl redirects the merchant to approve the charge. Source: lib/billing/request.ts:99-194

Request Parameters

interface BillingRequestParams {
  session: Session;
  plan: string;              // Plan name from config
  isTest?: boolean;          // Default: true
  returnUrl?: string;        // Where to redirect after approval
  returnObject?: boolean;    // Return full object vs just URL
}

Custom Return URL

const confirmationUrl = await shopify.billing.request({
  session,
  plan: 'Basic Plan',
  returnUrl: 'https://myapp.com/billing/success',
});
If not provided, embedded apps use the embedded app URL, non-embedded apps use the host URL. Source: lib/billing/request.ts:122-131

Billing Intervals

{
  lineItems: [
    {
      interval: BillingInterval.Every30Days,
      amount: 29.99,
      currencyCode: 'USD',
    },
  ],
  trialDays: 7, // Optional trial period
}
Available intervals:
  • BillingInterval.Every30Days - Monthly billing
  • BillingInterval.Annual - Yearly billing

Line Items

Subscriptions can include multiple line items:
billing: {
  'Hybrid Plan': {
    lineItems: [
      {
        interval: BillingInterval.Every30Days,
        amount: 20.00,
        currencyCode: 'USD',
      },
      {
        interval: BillingInterval.Usage,
        amount: 500.00,
        currencyCode: 'USD',
        terms: 'Additional usage charges',
      },
    ],
  },
}
Source: lib/billing/request.ts:204-252

Trial Periods

Offer free trials on recurring subscriptions:
billing: {
  'Premium Plan': {
    lineItems: [{
      interval: BillingInterval.Every30Days,
      amount: 49.99,
      currencyCode: 'USD',
    }],
    trialDays: 14,
  },
}

Discounts

Apply discounts to recurring charges:
billing: {
  'Annual Plan': {
    lineItems: [
      {
        interval: BillingInterval.Annual,
        amount: 240.00,
        currencyCode: 'USD',
        discount: {
          durationLimitInIntervals: 3, // 3 billing cycles
          value: {
            percentage: 20.0,          // 20% off
          },
        },
      },
    ],
  },
}
Source: lib/billing/request.ts:217-225

Usage Records

For usage-based billing, create usage records:
app.post('/api/track-usage', async (req, res) => {
  const session = await loadSession(req);
  const {amount, description} = req.body;
  
  await shopify.billing.createUsageRecord({
    session,
    subscriptionLineItemId: 'gid://shopify/AppSubscriptionLineItem/1',
    price: amount,
    description,
  });
  
  res.json({success: true});
});

Updating Usage Cap

Update the capped amount for usage billing:
await shopify.billing.updateUsageCappedAmount({
  session,
  subscriptionLineItemId: 'gid://shopify/AppSubscriptionLineItem/1',
  cappedAmount: 2000.00,
});
Source: lib/billing/index.ts:14-25

Canceling Subscriptions

Cancel an active subscription:
await shopify.billing.cancel({
  session,
  subscriptionId: 'gid://shopify/AppSubscription/1',
});

Getting Active Subscriptions

Retrieve all active subscriptions:
const subscriptions = await shopify.billing.subscriptions({
  session,
});

console.log(subscriptions);
// [
//   {
//     id: 'gid://shopify/AppSubscription/1',
//     name: 'Basic Plan',
//     test: false,
//     lineItems: [...]
//   }
// ]

Error Handling

import {BillingError} from '@shopify/shopify-api';

try {
  await shopify.billing.request({...});
} catch (error) {
  if (error instanceof BillingError) {
    console.error('Billing error:', error.message);
    console.error('Error data:', error.errorData);
  }
}
Common errors:
  • Plan not found in config
  • GraphQL user errors (validation failures)
  • Network errors
Source: lib/error.ts:116-128

Complete Example

import {shopifyApi, BillingInterval} from '@shopify/shopify-api';

const shopify = shopifyApi({
  apiKey: process.env.SHOPIFY_API_KEY,
  apiSecretKey: process.env.SHOPIFY_API_SECRET,
  scopes: ['read_products'],
  hostName: process.env.HOST,
  billing: {
    'Basic': {
      lineItems: [
        {
          interval: BillingInterval.Every30Days,
          amount: 10.00,
          currencyCode: 'USD',
        },
      ],
      trialDays: 7,
    },
    'Pro': {
      lineItems: [
        {
          interval: BillingInterval.Every30Days,
          amount: 30.00,
          currencyCode: 'USD',
        },
        {
          interval: BillingInterval.Usage,
          amount: 1000.00,
          currencyCode: 'USD',
          terms: 'Additional usage up to $1000',
        },
      ],
    },
  },
});

Best Practices

  • Always use isTest: true in development
  • Check for active payments on protected routes
  • Store subscription IDs for usage tracking
  • Handle billing errors gracefully
  • Provide clear upgrade paths
  • Use returnObject: true when you need subscription details
Billing must be configured in shopifyApi() config before using billing functions. Attempting to use billing without configuration throws a BillingError.Source: lib/billing/check.ts:44-48

Build docs developers (and LLMs) love