Shopify App Development: How to Raise Prices for New Installs Only

Shopify App Development: How to Raise Prices for New Installs Only

Introduction

You've built a Shopify app, launched it at an early-adopter price, and now it's time to raise rates. Maybe your costs have increased, you've added significant features, or you simply underpriced the app at launch. Whatever the reason, you face a critical constraint:

You cannot just change your plan price and call it done.

Existing subscribers who accepted a billing plan at a specific price have a legal and platform-level expectation of continuity. Changing their price without consent violates Shopify's Partner Program Agreement and, in many jurisdictions, consumer protection law. Do it wrong and you risk mass uninstalls, negative reviews, and Partner account consequences.

Done right, however, price increases are a normal, healthy part of app business growth. This guide shows you exactly how to implement them cleanly using Shopify's Billing API — protecting existing users while charging new installs your new rates.


Who This Guide Is For

  • Shopify app developers on the Partner Dashboard managing paid apps
  • Developers using the Shopify Billing API (appSubscriptionCreate, appPurchaseOneTimeCreate)
  • App builders transitioning from usage-based, flat-rate, or tiered pricing models
  • Developers preparing a price increase announcement for their user base

Core Concept: Plan Grandfathering

The correct approach is plan grandfathering — a pricing architecture where:

  • Existing subscribers stay on their current plan at their current price indefinitely (or until they voluntarily change plans)
  • New installs are presented only with the new, higher-priced plans
  • The old plans remain active in your billing logic but are hidden from the plan selection UI for new users

Shopify's Billing API supports this natively because subscription plans are created per-merchant at install time — they are not global catalog items that update for everyone simultaneously.


Understanding Shopify's Billing API Architecture

Before writing any code, understand how Shopify billing actually works:

How App Subscriptions Work

When a merchant installs your app and selects a plan, your app calls appSubscriptionCreate which:

  1. Creates a new subscription object specific to that merchant
  2. Returns an confirmationUrl that the merchant must visit to approve the charge
  3. Once approved, the subscription is active and billed on Shopify's cycle

Key insight: Each merchant's subscription is an independent object. There is no global plan definition that updates all subscribers simultaneously. This is what makes grandfathering possible — and also means price changes are 100% in your control.

Plan Types

Type API Mutation Use Case
Recurring flat rate appSubscriptionCreate Monthly/annual fixed plans
Usage-based appSubscriptionCreate with lineItems.plan.appUsagePricingDetails Pay-per-use or metered billing
One-time charge appPurchaseOneTimeCreate Lifetime deals, add-ons
Capped usage appSubscriptionCreate with a usage cap Usage with a monthly ceiling

Step-by-Step Implementation


Step 1 — Audit Your Current Plan Structure

Before making any changes, document your existing plan architecture:

  1. Log in to your Shopify Partner Dashboard
  2. Go to Apps → [Your App] → Analytics → Billing
  3. Note all active plan names, prices, and subscriber counts
  4. In your app's database, confirm how you store the plan each merchant is on

Create a spreadsheet with:

  • Plan name (e.g., "Basic", "Pro", "Enterprise")
  • Current price
  • Number of active subscribers on each plan
  • Date each plan was originally created

This is your grandfathering baseline — you need to know who is on what before you create new plans.


Step 2 — Create New Plans in Your App Code (Do NOT Modify Old Ones)

The fundamental rule: never edit the price of an existing plan definition in your code if merchants are actively subscribed to it.

Instead, create entirely new plan objects with new names or version identifiers.

Example — before (old plans):


const PLANS = {
  basic: {
    name: 'Basic',
    price: 9.99,
    interval: 'EVERY_30_DAYS',
    trialDays: 14
  },
  pro: {
    name: 'Pro',
    price: 24.99,
    interval: 'EVERY_30_DAYS',
    trialDays: 14
  }
};

Example — after (new plans added, old plans preserved):


const PLANS = {
  // LEGACY PLANS — grandfathered, existing subscribers only
  basic_v1: {
    name: 'Basic',
    price: 9.99,
    interval: 'EVERY_30_DAYS',
    trialDays: 14,
    legacy: true  // Flag to hide from new installs
  },
  pro_v1: {
    name: 'Pro',
    price: 24.99,
    interval: 'EVERY_30_DAYS',
    trialDays: 14,
    legacy: true
  },

  // NEW PLANS — shown to new installs only
  basic_v2: {
    name: 'Basic',
    price: 14.99,
    interval: 'EVERY_30_DAYS',
    trialDays: 14,
    legacy: false
  },
  pro_v2: {
    name: 'Pro',
    price: 34.99,
    interval: 'EVERY_30_DAYS',
    trialDays: 14,
    legacy: false
  }
};

Step 3 — Update Your Plan Selection UI Logic

Your plan selection screen (shown during onboarding and in plan upgrade flows) must filter plans based on whether the merchant is new or existing.

Logic:


function getAvailablePlans(merchant) {
  const isExistingSubscriber = merchant.currentPlan !== null;

  if (isExistingSubscriber) {
    // Show their current legacy plan + new plans (so they can upgrade)
    return Object.values(PLANS).filter(plan =>
      plan.id === merchant.currentPlan || !plan.legacy
    );
  } else {
    // New install — show only current plans
    return Object.values(PLANS).filter(plan => !plan.legacy);
  }
}

What this achieves:

  • New installs only see the new pricing — they have no way to select the old lower price
  • Existing subscribers see their current plan (so they know what they're on) alongside new plans (in case they want to upgrade)
  • No existing subscriber is forced onto a new plan

Step 4 — Handle the appSubscriptionCreate Call

When a new install selects a plan, call appSubscriptionCreate with the new pricing:


// GraphQL mutation — Shopify Billing API
const CREATE_SUBSCRIPTION = `
  mutation appSubscriptionCreate(
    $name: String!,
    $lineItems: [AppSubscriptionLineItemInput!]!,
    $returnUrl: URL!,
    $test: Boolean,
    $trialDays: Int
  ) {
    appSubscriptionCreate(
      name: $name,
      lineItems: $lineItems,
      returnUrl: $returnUrl,
      test: $test,
      trialDays: $trialDays
    ) {
      appSubscription {
        id
        status
      }
      confirmationUrl
      userErrors {
        field
        message
      }
    }
  }
`;

// For a new install on Basic v2
const variables = {
  name: 'Basic',           // Plan display name
  lineItems: [{
    plan: {
      appRecurringPricingDetails: {
        price: { amount: 14.99, currencyCode: 'USD' },
        interval: 'EVERY_30_DAYS'
      }
    }
  }],
  returnUrl: 'https://yourapp.com/confirm',
  test: false,
  trialDays: 14
};

Note: The name field in appSubscriptionCreate is what appears on the merchant's Shopify invoice. Keep it clean and recognizable — "Basic Plan", "Pro Plan", etc. Do not include version numbers in this field.


Step 5 — Store Plan Version in Your Database

Your app's database must track which plan version each merchant is on. This is critical for correctly rendering their UI and handling future upgrades.

Recommended database schema addition:


ALTER TABLE merchant_subscriptions
ADD COLUMN plan_key VARCHAR(50),        -- e.g., 'basic_v1', 'pro_v2'
ADD COLUMN plan_version INTEGER,        -- e.g., 1, 2
ADD COLUMN grandfathered BOOLEAN DEFAULT FALSE,
ADD COLUMN subscription_created_at TIMESTAMP;

On new install — record the plan version:


await db.merchantSubscriptions.upsert({
  shopDomain: shop,
  planKey: 'basic_v2',
  planVersion: 2,
  grandfathered: false,
  subscriptionCreatedAt: new Date()
});

On existing merchant — preserve their plan version:


// DO NOT update planKey or planVersion for existing subscribers
// Only update status fields (active, cancelled, etc.)
await db.merchantSubscriptions.update({
  where: { shopDomain: shop },
  data: { status: 'active' }  // Never overwrite planKey for existing merchants
});

Step 6 — Handle Voluntary Plan Changes for Existing Subscribers

If an existing grandfathered subscriber wants to upgrade to a higher plan, they should be moved to the new pricing tier — not a legacy version of the higher tier.

Logic:

  • Grandfathered Basic v1 merchant upgrades → moves to Pro v2 (new pricing)
  • They are no longer grandfathered once they voluntarily change plans
  • Clearly communicate this in your upgrade UI: "Upgrading will move you to our current Pro plan at $34.99/month."

Implementation — cancel old subscription and create new one:


async function upgradePlan(shop, newPlanKey) {
  const merchant = await getMerchant(shop);
  const newPlan = PLANS[newPlanKey];

  // Step 1: Cancel existing subscription
  await shopifyGraphQL(shop, CANCEL_SUBSCRIPTION, {
    id: merchant.activeSubscriptionId
  });

  // Step 2: Create new subscription at new price
  const result = await shopifyGraphQL(shop, CREATE_SUBSCRIPTION, {
    name: newPlan.name,
    lineItems: [{ plan: { appRecurringPricingDetails: {
      price: { amount: newPlan.price, currencyCode: 'USD' },
      interval: newPlan.interval
    }}}],
    returnUrl: `https://yourapp.com/confirm?plan=${newPlanKey}`,
    trialDays: 0  // No trial on upgrades
  });

  // Step 3: Redirect merchant to confirm new charge
  return result.data.appSubscriptionCreate.confirmationUrl;
}

Step 7 — Communicate the Price Change to Existing Users

Even though you're not changing existing subscribers' billing, proactive communication builds trust and reduces churn from shocked merchants who notice a price difference.

Recommended communication sequence:

30 days before launch:

  • Email all existing subscribers
  • Announce the upcoming price increase for new users
  • Emphasize they are grandfathered and their price will not change
  • Optional: offer existing users a chance to upgrade to new plans at their grandfathered rate for a limited window

On launch day:

  • Update your App Store listing with new pricing
  • Send a follow-up email confirming the change is live
  • Update your in-app plan page to clearly label grandfathered plans

In-app messaging:


🔒 Your Grandfathered Plan
You're on our legacy Basic plan at $9.99/month.
New customers pay $14.99/month for the same plan.
Your pricing is locked in as long as you stay subscribed.

Step 8 — Update Your Shopify App Store Listing

Your App Store listing must reflect current pricing for new installs:

  1. Log in to Partner Dashboard → Apps → [Your App] → App listing
  2. Update the Pricing section with your new plan prices
  3. Update your pricing page or feature comparison table screenshots
  4. Update your app's description if it mentions pricing
  5. Submit for Shopify review if required (listing changes may trigger a review)

Important: The App Store listing shows pricing to potential new merchants. Existing merchants are billed according to their active subscription — not the listing price.


Step 9 — Test Everything in a Development Store

Before going live, test the complete flow:

Test checklist:

  • New install sees only v2 plans in plan selection UI
  • New install can subscribe to a v2 plan and confirm billing
  • Existing v1 subscriber still sees their v1 plan in their account page
  • Existing v1 subscriber's billing has not changed
  • Existing v1 subscriber can upgrade to v2 plan
  • After upgrade, subscriber is billed at v2 price
  • Downgrade logic (if supported) works correctly
  • Cancellation and reinstall shows v2 pricing (not v1)
  • Webhook handlers (app/subscriptions/update) handle both v1 and v2 plan keys correctly

Use Shopify's test mode:


// Set test: true in appSubscriptionCreate during testing
const variables = {
  // ...
  test: true  // No real charge in development/test stores
};

Common Mistakes to Avoid

❌ Modifying existing plan prices in your code without versioning If you change basic.price from 9.99 to 14.99 in your plan config, any merchant who reinstalls or has their subscription renewed may be charged the new price unexpectedly.

❌ Forgetting to handle reinstalls If a grandfathered merchant uninstalls and reinstalls your app, they are treated as a new install by Shopify. They lose their grandfathered status and are presented with current pricing. This is expected behavior — make it clear in your communication that grandfathering applies to continuous subscriptions only.

❌ Showing version numbers to merchants: Plan names like "Basic v1" and "Basic v2" confuse merchants. Use the legacy flag internally but display clean names in the UI.

❌ Not handling the confirmation URL flow: Every new subscription requires merchant approval via confirmationUrl. If you skip this step or handle the redirect incorrectly, the subscription is never activated.

❌ Changing prices mid-billing-cycle: Never cancel and recreate a subscription mid-cycle to force a price change. Always let the current cycle complete, or explicitly communicate any prorated changes.


Architecture Recap


New Install
    ↓
Plan Selection UI → Filter: legacy: false → Shows v2 plans only
    ↓
appSubscriptionCreate (v2 price) → confirmationUrl → Merchant approves
    ↓
DB: planKey = 'basic_v2', planVersion = 2, grandfathered = false

─────────────────────────────────────────────────

Existing Subscriber (Grandfathered)
    ↓
Plan Page UI → Shows current v1 plan (locked) + v2 plans (optional upgrade)
    ↓
No billing change unless merchant voluntarily upgrades
    ↓
If upgrade: cancel v1 sub → create v2 sub → confirmationUrl → approve
    ↓
DB: planKey updated to 'pro_v2', grandfathered = false
Back to blog

Leave a comment