← Shopify App BuilderCONTENT HISTORY

Update to Shopify App Builder

Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Implement recurring, usage-based, one-time, and hybrid Shopify app billing. Covers appSubscriptionCreate, appUsageRecordCreate, trials, capped amounts, replacement behavior, test mode, current revenue-share rules, and pricing tiers.",
  "included_files": [],
  "name": "app-billing",
  "skill_md_contents": "---\nname: app-billing\ndescription: \"Implement recurring, usage-based, one-time, and hybrid Shopify app billing. Covers appSubscriptionCreate, appUsageRecordCreate, trials, capped amounts, replacement behavior, test mode, current revenue-share rules, and pricing tiers.\"\n---\n\n# Shopify App Billing\n\nShopify apps can charge merchants through Shopify's billing system. Charges are collected via the merchant's Shopify payment method and appear on their bill. Three models are supported: **recurring** (fixed monthly/annual), **usage-based** (pay-per-action), and **one-time** (one-off charges). You can also combine them (hybrid).\n\n## Revenue Share Model\n\nFor developers eligible for Shopify's standard rates:\n- You keep **100% of the first $1,000,000 USD in lifetime gross app revenue earned from January 1, 2025**.\n- Above that threshold, Shopify's revenue share is **15%**, so you keep **85%** before other fees and taxes.\n- All billing is separately subject to a **2.9% processing fee**, applicable sales tax, and potentially regional regulatory fees.\n- Shopify applies special eligibility rules to very large developers and aggregates revenue across associated developer accounts. Verify the current policy before financial modeling.\n\nThis means your pricing directly affects what you keep:\n```\nApp charges $10 while eligible for the 0% revenue-share tier\n├─ Merchant pays: $10.00\n├─ Processing fee: $0.29, before tax or regional fees\n└─ Developer amount before tax/regional fees: $9.71\n\nApp charges $100 above the $1M lifetime threshold\n├─ Merchant pays: $100.00\n├─ Revenue share: $15.00\n├─ Processing fee: $2.90, before tax or regional fees\n└─ Developer amount before tax/regional fees: $82.10\n```\n\n**Planning rule:** Model revenue share, processing fees, taxes, refunds, and cost to serve separately; don't assume gross charges equal payout.\n\n## Billing Models\n\n### 1. Recurring (Fixed Interval)\n\n**Use Case:** Subscription model (e.g., \"Pro plan $99/month\")\n**Charging:** First charge immediate on approval; subsequent charges on anniversary date\n**Cancellation:** Merchant can cancel anytime; charged through current period end\n\n**appSubscriptionCreate Mutation:**\n```graphql\nmutation CreateRecurringSubscription {\n  appSubscriptionCreate(\n    input: {\n      trialDays: 7\n      lineItems: [\n        {\n          plan: {\n            appRecurringPricingDetails: {\n              interval: MONTHLY  # or ANNUAL\n              price: { amount: \"9.99\", currencyCode: \"USD\" }\n            }\n          }\n        }\n      ]\n      returnUrl: \"https://your-app.com/billing/confirm\"\n    }\n  ) {\n    appSubscription {\n      id\n      confirmationUrl  # Merchant must visit this URL to approve\n      lineItems {\n        id\n        plan {\n          pricingDetails {\n            ... on AppRecurringPricingDetails {\n              interval\n              price { amount currencyCode }\n            }\n          }\n        }\n      }\n      status  # PENDING, ACTIVE, DECLINED, EXPIRED, FROZEN, CANCELLED\n      currentPeriodEnd\n      trialDays\n      trialEndsOn\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n```\n\n**Remix Implementation (Recurring Billing):**\n```typescript\nimport { json, redirect } from '@shopify/remix-oxygen';\nimport { authenticate } from '~/shopify.server';\n\nexport const action = async ({ request }) => {\n  if (request.method !== 'POST') return json({ error: 'POST only' }, { status: 405 });\n\n  const { admin } = await authenticate.admin(request);\n\n  const response = await admin.graphql(`\n    mutation CreateRecurringSubscription($input: AppSubscriptionInput!) {\n      appSubscriptionCreate(input: $input) {\n        appSubscription {\n          id\n          confirmationUrl\n          status\n          currentPeriodEnd\n          lineItems {\n            id\n            plan {\n              pricingDetails {\n                ... on AppRecurringPricingDetails {\n                  interval\n                  price { amount currencyCode }\n                }\n              }\n            }\n          }\n        }\n        userErrors {\n          field\n          message\n        }\n      }\n    }\n  `, {\n    variables: {\n      input: {\n        trialDays: 7,\n        lineItems: [\n          {\n            plan: {\n              appRecurringPricingDetails: {\n                interval: 'MONTHLY',\n                price: { amount: '9.99', currencyCode: 'USD' },\n              },\n            },\n          },\n        ],\n        returnUrl: 'https://your-app.com/billing/confirm',\n      },\n    },\n  });\n\n  const { appSubscription, userErrors } = response.data?.appSubscriptionCreate || {};\n\n  if (userErrors?.length > 0) {\n    return json({ errors: userErrors }, { status: 400 });\n  }\n\n  // Merchant must visit confirmation URL to approve billing\n  return redirect(appSubscription.confirmationUrl);\n};\n\nexport const loader = async ({ request }) => {\n  const { admin, session } = await authenticate.admin(request);\n  const url = new URL(request.url);\n  const charge = url.searchParams.get('charge_id');\n\n  if (!charge) {\n    return json({ message: 'Waiting for merchant approval' });\n  }\n\n  // Query subscription status after merchant approves\n  const response = await admin.graphql(`\n    query GetSubscription($id: ID!) {\n      appSubscription(id: $id) {\n        id\n        status\n        currentPeriodEnd\n        returnUrl\n        lineItems {\n          id\n          plan {\n            pricingDetails {\n              ... on AppRecurringPricingDetails {\n                interval\n                price { amount currencyCode }\n              }\n            }\n          }\n        }\n      }\n    }\n  `, {\n    variables: { id: charge },\n  });\n\n  const subscription = response.data?.appSubscription;\n\n  if (subscription?.status === 'ACTIVE') {\n    return json({ success: true, subscription });\n  }\n\n  return json({ success: false, subscription });\n};\n```\n\n### 2. Usage-Based (Pay-Per-Action)\n\n**Use Case:** Charge per email sent, API call, report generated, etc.\n**Charging:** Metered; merchant is charged monthly for accumulated usage\n**Cap Amount:** Optional; maximum the merchant can be charged per period\n\n**appSubscriptionCreate Mutation (Usage-Based):**\n```graphql\nmutation CreateUsageSubscription {\n  appSubscriptionCreate(\n    input: {\n      trialDays: 0\n      lineItems: [\n        {\n          plan: {\n            appUsagePricingDetails: {\n              cappedAmount: {\n                amount: \"100.00\"  # Max charge per billing period\n                currencyCode: \"USD\"\n              }\n              terms: \"$0.01 per email sent\"  # Display string\n            }\n          }\n        }\n      ]\n      returnUrl: \"https://your-app.com/billing/confirm\"\n    }\n  ) {\n    appSubscription {\n      id\n      confirmationUrl\n      lineItems {\n        id\n        plan {\n          pricingDetails {\n            ... on AppUsagePricingDetails {\n              cappedAmount { amount currencyCode }\n              terms\n            }\n          }\n        }\n      }\n      status\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n```\n\n**Recording Usage (appUsageRecordCreate):**\n```graphql\nmutation RecordUsage($subscriptionLineId: ID!, $quantity: Float!, $idempotencyKey: String!) {\n  appUsageRecordCreate(\n    subscriptionLineId: $subscriptionLineId\n    quantity: $quantity\n    idempotencyKey: $idempotencyKey\n  ) {\n    appUsageRecord {\n      id\n      createdAt\n      quantity\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}\n```\n\n**Remix Implementation (Usage-Based):**\n```typescript\nimport { json } from '@shopify/remix-oxygen';\nimport { authenticate } from '~/shopify.server';\n\n// Step 1: Create usage-based subscription\nexport const action = async ({ request }) => {\n  if (request.method !== 'POST') return json({ error: 'POST only' }, { status: 405 });\n\n  const { admin } = await authenticate.admin(request);\n\n  const response = await admin.graphql(`\n    mutation CreateUsageSubscription($input: AppSubscriptionInput!) {\n      appSubscriptionCreate(input: $input) {\n        appSubscription {\n          id\n          confirmationUrl\n          lineItems {\n            id\n            plan {\n              pricingDetails {\n                ... on AppUsagePricingDetails {\n                  cappedAmount { amount currencyCode }\n                  terms\n                }\n              }\n            }\n          }\n        }\n        userErrors { field message }\n      }\n    }\n  `, {\n    variables: {\n      input: {\n        lineItems: [\n          {\n            plan: {\n              appUsagePricingDetails: {\n                cappedAmount: { amount: '100.00', currencyCode: 'USD' },\n                terms: '$0.10 per report generated',\n              },\n            },\n          },\n        ],\n        returnUrl: 'https://your-app.com/billing/confirm',\n      },\n    },\n  });\n\n  const { appSubscription } = response.data?.appSubscriptionCreate || {};\n  return redirect(appSubscription.confirmationUrl);\n};\n\n// Step 2: Record usage when action occurs (e.g., report generated)\nexport const recordUsage = async (\n  admin,\n  subscriptionLineId: string,\n  quantity: number\n) => {\n  const idempotencyKey = `${subscriptionLineId}-${Date.now()}`; // Prevent duplicates\n\n  const response = await admin.graphql(`\n    mutation RecordUsage(\n      $subscriptionLineId: ID!\n      $quantity: Float!\n      $idempotencyKey: String!\n    ) {\n      appUsageRecordCreate(\n        subscriptionLineId: $subscriptionLineId\n        quantity: $quantity\n        idempotencyKey: $idempotencyKey\n      ) {\n        appUsageRecord { id createdAt quantity }\n        userErrors { field message }\n      }\n    }\n  `, {\n    variables: {\n      subscriptionLineId,\n      quantity,\n      idempotencyKey,\n    },\n  });\n\n  const { appUsageRecord, userErrors } = response.data?.appUsageRecordCreate || {};\n\n  if (userErrors?.length > 0) {\n    console.error('Usage record error:', userErrors);\n    return null;\n  }\n\n  return appUsageRecord;\n};\n\n// Step 3: In report generation endpoint\nexport const generateReport = async ({ request }) => {\n  const { admin, session } = await authenticate.admin(request);\n  const data = await request.json();\n\n  // Generate report\n  const reportId = await createReport(session.shop, data);\n\n  // Record usage (charge for report)\n  const subscriptionLineId = 'gid://shopify/AppSubscriptionLine/123';\n  await recordUsage(admin, subscriptionLineId, 1); // 1 report = 1 charge unit\n\n  return json({ success: true, reportId });\n};\n```\n\n### 3. One-Time Charge\n\n**Use Case:** Upfront license purchase, setup fee, premium feature unlocks\n**Charging:** Immediate; merchant approves and is charged once\n**No Recurring:** Does not repeat; separate call required for each charge\n\n**appSubscriptionCreate Mutation (One-Time):**\n```graphql\nmutation CreateOneTimeCharge {\n  appSubscriptionCreate(\n    input: {\n      lineItems: [\n        {\n          plan: {\n            appOneTimePricingDetails: {\n              price: { amount: \"49.99\", currencyCode: \"USD\" }\n            }\n          }\n        }\n      ]\n      returnUrl: \"https://your-app.com/billing/confirm\"\n    }\n  ) {\n    appSubscription {\n      id\n      confirmationUrl\n      lineItems {\n        id\n        plan {\n          pricingDetails {\n            ... on AppOneTimePricingDetails {\n              price { amount currencyCode }\n            }\n          }\n        }\n      }\n      status\n    }\n    userErrors { field message }\n  }\n}\n```\n\n### 4. Hybrid (Recurring + Usage-Based)\n\n**Use Case:** Base subscription + pay-per-extra-action (e.g., \"Pro $99/month + $0.05 per extra report\")\n**Charging:** Base charge monthly + usage charges accumulated during month\n\n**appSubscriptionCreate Mutation (Hybrid):**\n```graphql\nmutation CreateHybridSubscription {\n  appSubscriptionCreate(\n    input: {\n      lineItems: [\n        {\n          plan: {\n            appRecurringPricingDetails: {\n              interval: MONTHLY\n              price: { amount: \"99.00\", currencyCode: \"USD\" }\n            }\n          }\n        },\n        {\n          plan: {\n            appUsagePricingDetails: {\n              cappedAmount: { amount: \"1000.00\", currencyCode: \"USD\" }\n              terms: \"$0.05 per extra report (beyond 100/month)\"\n            }\n          }\n        }\n      ]\n      returnUrl: \"https://your-app.com/billing/confirm\"\n    }\n  ) {\n    appSubscription {\n      id\n      confirmationUrl\n      lineItems { id plan { pricingDetails { ... on AppRecurringPricingDetails { interval price { amount currencyCode } } ... on AppUsagePricingDetails { cappedAmount { amount currencyCode } terms } } } }\n    }\n    userErrors { field message }\n  }\n}\n```\n\n## Billing Configuration in Remix\n\n**Require Billing (MUST_USE_BILLING):**\n\nIn your Remix root component, block access until billing is confirmed:\n\n```typescript\n// app/root.tsx\nimport { authenticate } from '~/shopify.server';\n\nexport const loader = async ({ request }) => {\n  const { billing } = await authenticate.admin(request);\n\n  // MUST_USE_BILLING: Prevent app usage without active subscription\n  await billing.require({\n    plans: ['basic', 'premium', 'unlimited'], // At least one required\n    onFailUrl: '/billing', // Redirect if no active subscription\n  });\n\n  return null;\n};\n```\n\n**Request Billing (Optional):**\n\nAllow app usage but encourage upgrade:\n\n```typescript\nexport const loader = async ({ request }) => {\n  const { billing } = await authenticate.admin(request);\n\n  // OPTIONAL: App works without billing, but show upsell\n  const response = await billing.request({\n    plan: 'premium',\n    isTest: false,\n  });\n\n  return json({ needsBilling: !response.appSubscription });\n};\n```\n\n**Test Mode:**\n\nSimulate billing without charging:\n\n```typescript\nexport const loader = async ({ request }) => {\n  const { billing } = await authenticate.admin(request);\n\n  // Test mode: Billing is mocked; no real charges\n  await billing.require({\n    plans: ['test-plan'],\n    isTest: true, // Set to false for production\n  });\n\n  return null;\n};\n```\n\n**Cancel Subscription:**\n\n```graphql\nmutation CancelSubscription($id: ID!) {\n  appSubscriptionCancel(id: $id) {\n    appSubscription {\n      id\n      status  # CANCELLED\n      returnUrl\n    }\n    userErrors { field message }\n  }\n}\n```\n\n## Pricing Strategy & Tier Ladder\n\n**Common Pricing Models:**\n\n### Flat Pricing (Simple)\n```\nStarter: $29/month (up to 100 products)\nPro: $99/month (up to 10,000 products)\nEnterprise: $499/month (unlimited)\n```\n\n### Tiered Usage (Pay-Per-Action)\n```\nBase: $0/month (free tier, 100 reports/month)\nStandard: $0.05 per report above 100\nPremium: $0.02 per report above 100 (volume discount)\n```\n\n### Freemium + Paid Features\n```\nFree: $0 (basic features only)\nPlus: $9.99/month (advanced analytics)\nPro: $49.99/month (API access + custom integrations)\n```\n\n### Feature-Gated Tiers\n```\nStandard: $49/month\n├─ Dashboard\n├─ Email notifications\n└─ 30-day history\n\nPro: $149/month\n├─ Everything in Standard\n├─ API access\n├─ Custom rules\n└─ Unlimited history\n```\n\n**Recommended Tier Ladder:**\n1. **Free Tier** (required; builds trust)\n   - Basic functionality\n   - Limited usage (e.g., 10 actions/month)\n   - No integrations\n   - Community support only\n\n2. **Starter** ($19-49/month)\n   - 1-2 intermediate features\n   - Moderate usage (e.g., 1,000 actions/month)\n   - Email support\n\n3. **Professional** ($99-299/month)\n   - All features\n   - High usage (unlimited or 100K+ actions)\n   - Priority support\n   - API access\n\n4. **Enterprise** (custom pricing)\n   - Custom features\n   - Dedicated support\n   - SLA guarantee\n   - White-label option\n\n**Pricing Psychology:**\n- Odd pricing ($19.99 vs $20) increases conversion\n- Annual pricing 20-30% cheaper than monthly (increases LTV)\n- \"Pro\" tier should be sweet spot; most conversions\n- Free tier must have real value; 10-15% convert to paid\n\n## Full Working Examples\n\n### Example 1: Flat Recurring Billing (3-Tier Plan)\n\n**routes/billing/create.tsx (Create Subscription):**\n```typescript\nimport { json, redirect } from '@shopify/remix-oxygen';\nimport { authenticate } from '~/shopify.server';\nimport { Form } from '@remix-run/react';\n\nexport const action = async ({ request }) => {\n  if (request.method !== 'POST') return json({ error: 'POST only' }, { status: 405 });\n\n  const formData = await request.formData();\n  const plan = formData.get('plan') as string;\n  const { admin } = await authenticate.admin(request);\n\n  const planConfig = {\n    starter: { price: '29.99', interval: 'MONTHLY' },\n    pro: { price: '99.99', interval: 'MONTHLY' },\n    enterprise: { price: '499.99', interval: 'MONTHLY' },\n  };\n\n  const config = planConfig[plan as keyof typeof planConfig];\n  if (!config) return json({ error: 'Invalid plan' }, { status: 400 });\n\n  const response = await admin.graphql(`\n    mutation CreateSubscription($input: AppSubscriptionInput!) {\n      appSubscriptionCreate(input: $input) {\n        appSubscription { id confirmationUrl status }\n        userErrors { field message }\n      }\n    }\n  `, {\n    variables: {\n      input: {\n        lineItems: [\n          {\n            plan: {\n              appRecurringPricingDetails: {\n                interval: config.interval,\n                price: { amount: config.price, currencyCode: 'USD' },\n              },\n            },\n          },\n        ],\n        returnUrl: 'https://your-app.com/billing/confirm',\n      },\n    },\n  });\n\n  const { appSubscription } = response.data?.appSubscriptionCreate || {};\n  return redirect(appSubscription.confirmationUrl);\n};\n\nexport const loader = async ({ request }) => {\n  await authenticate.admin(request);\n  return null;\n};\n\nexport default function BillingPlans() {\n  return (\n    <div>\n      <h1>Choose Your Plan</h1>\n      <Form method=\"post\">\n        <label>\n          <input type=\"radio\" name=\"plan\" value=\"starter\" /> Starter - $29/month\n        </label>\n        <label>\n          <input type=\"radio\" name=\"plan\" value=\"pro\" /> Pro - $99/month\n        </label>\n        <label>\n          <input type=\"radio\" name=\"plan\" value=\"enterprise\" /> Enterprise - $499/month\n        </label>\n        <button type=\"submit\">Subscribe</button>\n      </Form>\n    </div>\n  );\n}\n```\n\n### Example 2: Usage-Based Billing (Per-Report)\n\n**routes/api/report-create.tsx (Record Usage):**\n```typescript\nimport { json } from '@shopify/remix-oxygen';\nimport { authenticate } from '~/shopify.server';\nimport { prisma } from '~/db.server';\n\nexport const action = async ({ request }) => {\n  if (request.method !== 'POST') return json({ error: 'POST only' }, { status: 405 });\n\n  const { admin, session } = await authenticate.admin(request);\n  const data = await request.json();\n\n  // Generate report\n  const report = await prisma.report.create({\n    data: {\n      shop: session.shop,\n      name: data.name,\n      generatedAt: new Date(),\n    },\n  });\n\n  // Get subscription line for usage tracking\n  const subscription = await getActiveSubscription(session.shop);\n  if (subscription?.usageLineId) {\n    // Record 1 report = 1 usage unit (charge $0.10)\n    await admin.graphql(`\n      mutation RecordUsage(\n        $subscriptionLineId: ID!\n        $quantity: Float!\n        $idempotencyKey: String!\n      ) {\n        appUsageRecordCreate(\n          subscriptionLineId: $subscriptionLineId\n          quantity: $quantity\n          idempotencyKey: $idempotencyKey\n        ) {\n          appUsageRecord { id quantity }\n          userErrors { field message }\n        }\n      }\n    `, {\n      variables: {\n        subscriptionLineId: subscription.usageLineId,\n        quantity: 1,\n        idempotencyKey: `${report.id}-${Date.now()}`,\n      },\n    });\n  }\n\n  return json({ success: true, reportId: report.id });\n};\n\nasync function getActiveSubscription(shop: string) {\n  return prisma.subscription.findUnique({\n    where: { shop },\n    select: { usageLineId: true },\n  });\n}\n```\n\n### Example 3: Freemium + Paid Features (Feature Gates)\n\n**routes/app.reports.tsx (Feature-Gated Page):**\n```typescript\nimport { json } from '@shopify/remix-oxygen';\nimport { useLoaderData } from '@remix-run/react';\nimport { authenticate } from '~/shopify.server';\n\nexport const loader = async ({ request }) => {\n  const { admin, billing, session } = await authenticate.admin(request);\n\n  // Check if merchant has active paid subscription\n  const { appSubscriptions } = await admin.graphql(`\n    query {\n      appSubscriptions(first: 1) {\n        edges {\n          node {\n            id\n            status\n            lineItems {\n              plan {\n                pricingDetails {\n                  ... on AppRecurringPricingDetails {\n                    price { amount }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  `);\n\n  const isPaid = appSubscriptions?.edges?.[0]?.node?.status === 'ACTIVE';\n\n  // Load reports\n  const reports = await getReports(session.shop);\n\n  return json({ reports, isPaid });\n};\n\nexport default function ReportsPage() {\n  const { reports, isPaid } = useLoaderData<typeof loader>();\n\n  return (\n    <div>\n      <h1>Reports</h1>\n      {isPaid ? (\n        <div>\n          {/* Show all reports */}\n          {reports.map((r) => (\n            <div key={r.id}>{r.name}</div>\n          ))}\n        </div>\n      ) : (\n        <div className=\"upgrade-banner\">\n          <p>Unlock unlimited reports with Pro plan</p>\n          <a href=\"/billing\">Upgrade Now</a>\n        </div>\n      )}\n    </div>\n  );\n}\n\nasync function getReports(shop: string) {\n  // Fetch from database\n  return [];\n}\n```\n\n## Handle Subscription Lifecycle\n\n**Webhook: app/subscribed**\n\nWhen merchant approves subscription:\n\n```typescript\n// webhooks/app-subscribed.ts\nexport const webhooks = {\n  APP_SUBSCRIBED: {\n    deliveryMethod: DeliveryMethod.Http,\n    callbackUrl: '/webhooks/app-subscribed',\n  },\n};\n\nexport async function handleAppSubscribed(shop, body) {\n  const { appSubscription } = JSON.parse(body);\n\n  // Store subscription in database\n  await storeSubscription(shop, {\n    subscriptionId: appSubscription.id,\n    status: appSubscription.status,\n    currentPeriodEnd: appSubscription.currentPeriodEnd,\n    lineItems: appSubscription.lineItems,\n  });\n\n  // Send confirmation email\n  await sendEmail(shop, 'Subscription activated');\n}\n```\n\n**Webhook: billing_attempt.failure**\n\nWhen payment fails:\n\n```typescript\nexport async function handleBillingFailure(shop, body) {\n  const { appSubscription } = JSON.parse(body);\n\n  // Notify merchant\n  await sendEmail(shop, 'Payment failed; please update payment method');\n\n  // Optionally freeze features after multiple failures\n  await disableFeatures(shop);\n}\n```\n\n**Cleanup on Uninstall:**\n\n```typescript\nexport async function handleAppUninstalled(shop) {\n  // Delete billing records for this merchant\n  await prisma.subscription.deleteMany({ where: { shop } });\n  await prisma.usageRecord.deleteMany({ where: { shop } });\n}\n```\n\n## Troubleshooting\n\n| Issue | Cause | Fix |\n|-------|-------|-----|\n| **\"Invalid currency code\"** | Currency not supported by Shopify | Use USD, EUR, GBP, CAD, AUD, JPY, or merchant's shop currency |\n| **Billing confirmation URL returns 404** | Merchant link expired (24h limit) | Generate new confirmation URL; store in database with expiry |\n| **appUsageRecordCreate returns \"invalid subscription line\"** | Wrong subscriptionLineId | Query active subscription to get correct lineId |\n| **\"subscription is frozen\"** | Payment failed; account suspended | Fix payment method; contact Shopify support |\n| **Test mode billing not mocking** | isTest flag not set | Set `isTest: true` in billing.require() |\n| **Duplicate usage charges** | No idempotencyKey or not unique | Generate unique key per usage record; include timestamp |\n\n## Resources\n\n- **Shopify app billing:** https://shopify.dev/docs/apps/launch/billing\n- **GraphQL billing objects and mutations:** https://shopify.dev/docs/api/admin-graphql/latest/objects/AppSubscription\n- **Revenue share:** https://shopify.dev/docs/apps/launch/distribution/revenue-share\n- **Shopify App Store listing:** https://shopify.dev/docs/apps/launch/shopify-app-store/app-listing\n"
}

SHA-256 of public snapshot: c365f5f10b5045c4ba19820a3bc510a87512cc2056e5dd17587f25ab2b69c56a