6 min read

How to Track Expansion Revenue in Stripe

Expansion revenue—when your existing customers spend more—is the most profitable growth vector in SaaS. But Stripe doesn't have a built-in expansion revenue report. You need to track subscription changes and calculate the revenue delta yourself.

Set Up Subscription Metadata to Track Upgrades

Stripe subscriptions are the foundation of expansion tracking. You'll need to store the customer's previous plan value so you can calculate the increase when they upgrade.

Store the baseline plan amount in subscription metadata when creating or updating

When a customer upgrades their subscription, capture their previous MRR in the subscription's metadata object. This gives you a reference point to measure expansion. Use the Stripe dashboard or API to add custom key-value pairs.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// When a customer upgrades, update the subscription metadata
await stripe.subscriptions.update('sub_1234567890', {
  metadata: {
    previous_mrr: '99',
    upgrade_date: new Date().toISOString(),
    upgrade_type: 'plan_upgrade'
  }
});
Store upgrade context in subscription metadata for later analysis

Listen for subscription.updated events to catch all upgrades

Stripe emits a webhook whenever a subscription changes. The previous_attributes field tells you what changed. Capture these events to detect expansion and track timing.

javascript
const handleSubscriptionUpdated = async (event) => {
  const subscription = event.data.object;
  const previousAttributes = event.data.previous_attributes;

  if (previousAttributes.items) {
    const oldAmount = previousAttributes.items.data[0]?.price?.unit_amount || 0;
    const newAmount = subscription.items.data[0]?.price?.unit_amount || 0;
    
    if (newAmount > oldAmount) {
      const expansion = newAmount - oldAmount;
      console.log(`Expansion detected: +$${(expansion / 100).toFixed(2)}/month`);
    }
  }
};

app.post('/webhooks/stripe', bodyParser.raw({type: 'application/json'}), (req, res) => {
  const event = JSON.parse(req.body);
  if (event.type === 'customer.subscription.updated') {
    handleSubscriptionUpdated(event);
  }
  res.send({received: true});
});
Detect subscription upgrades via previous_attributes in webhook events
Watch out: The previous_attributes object only includes fields that changed. If you added a new line item (like an add-on), you'll see the updated items array, not a direct price change.

Calculate Expansion Revenue from Invoice Changes

The cleanest way to track expansion is to compare consecutive invoices for the same customer. If Invoice B is higher than Invoice A, the difference is your expansion.

Fetch and compare consecutive paid invoices

Query a customer's invoices in chronological order. Compare each paid invoice to the previous one. If the new invoice is higher, that's expansion revenue. Filter out one-time charges and focus on recurring MRR.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const calculateExpansion = async (customerId) => {
  const invoices = await stripe.invoices.list({
    customer: customerId,
    status: 'paid',
    limit: 100
  });

  let totalExpansion = 0;
  let previousAmount = 0;

  invoices.data.reverse().forEach((invoice) => {
    const currentAmount = invoice.amount_paid;
    if (currentAmount > previousAmount) {
      totalExpansion += currentAmount - previousAmount;
    }
    previousAmount = currentAmount;
  });

  return totalExpansion / 100; // Convert cents to dollars
};

const expansion = await calculateExpansion('cus_1234567890');
console.log(`Total expansion: $${expansion.toFixed(2)}`);

Filter for recurring charges only (exclude setup fees and one-time invoices)

Expansion is specifically the increase in recurring MRR, not one-time adjustments. Check the billing_reason field and sum only subscription line items to isolate recurring charges.

javascript
const getRecurringAmount = (invoice) => {
  // Only count subscription billing cycles
  if (invoice.billing_reason !== 'subscription_cycle') return 0;
  if (!invoice.subscription) return 0;

  // Sum only subscription line items (recurring charges)
  return invoice.lines.data
    .filter(line => line.type === 'subscription')
    .reduce((sum, line) => sum + line.amount, 0);
};

const calculateRecurringExpansion = async (customerId) => {
  const invoices = await stripe.invoices.list({
    customer: customerId,
    status: 'paid',
    limit: 100
  });

  let totalExpansion = 0;
  let previousRecurringAmount = 0;

  invoices.data.reverse().forEach((invoice) => {
    const currentRecurringAmount = getRecurringAmount(invoice);
    if (currentRecurringAmount > previousRecurringAmount) {
      totalExpansion += currentRecurringAmount - previousRecurringAmount;
    }
    previousRecurringAmount = currentRecurringAmount;
  });

  return totalExpansion / 100;
};
Filter invoices by billing_reason and line type to isolate recurring MRR increases
Tip: Use the invoice's created timestamp to align expansion with the calendar month it occurred. This makes it easier to report monthly expansion metrics and spot trends.

Identify and Tag High-Expansion Customers

Once you're calculating expansion, mark your best expansion customers in Stripe so you can segment and report on them without recalculating.

Query customers and aggregate expansion by segment

Loop through customers to calculate their total expansion. Group by metadata tags (like segment or plan_tier) to see which customer segments are driving the most growth.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const getExpansionBySegment = async (segment) => {
  const customers = await stripe.customers.list({ limit: 100 });
  const expansionByCustomer = {};
  let totalSegmentExpansion = 0;

  for (const customer of customers.data) {
    if (customer.metadata?.segment !== segment) continue;
    
    const expansion = await calculateRecurringExpansion(customer.id);
    if (expansion > 0) {
      expansionByCustomer[customer.id] = {
        name: customer.name,
        email: customer.email,
        expansion: expansion
      };
      totalSegmentExpansion += expansion;
    }
  }

  return {
    segment,
    total_expansion: totalSegmentExpansion,
    customer_count: Object.keys(expansionByCustomer).length,
    customers: expansionByCustomer
  };
};

const result = await getExpansionBySegment('saas');
console.log(`SaaS segment expansion: $${result.total_expansion.toFixed(2)}`);

Store expansion metrics in customer metadata for easy filtering

Once you've calculated expansion, tag the customer in Stripe with their expansion metrics. This lets you filter and report on expansion customers in the Stripe dashboard without recalculating.

javascript
// Tag expansion customers in Stripe for reporting
const markExpansionCustomer = async (customerId, expansionAmount, reason) => {
  await stripe.customers.update(customerId, {
    metadata: {
      expansion_revenue_annual: (expansionAmount * 12).toString(),
      expansion_reason: reason, // 'plan_upgrade', 'add_on', 'usage_increase'
      expansion_status: 'tracked',
      last_expansion_check: new Date().toISOString()
    }
  });
};

// After calculating expansion, update the customer
const expansion = await calculateRecurringExpansion('cus_1234567890');
if (expansion > 0) {
  await markExpansionCustomer(
    'cus_1234567890',
    expansion,
    'plan_upgrade'
  );
}
Tag expansion customers in Stripe metadata for segmentation and reporting
Watch out: Don't conflate expansion revenue with churn reversal. If a customer churned and re-subscribed at a higher price, that's reactivation, not expansion. Filter out customers with subscription gaps.

Common Pitfalls

  • Treating prorated invoices as full expansion. Stripe auto-prorates charges when you upgrade mid-cycle. Compare full billing cycles, not individual prorated invoices, to see true expansion.
  • Not accounting for discounts and coupons. A subscription amount can increase by $100, but if you apply a $100 coupon, the actual amount_paid stays flat. Check the invoice itself, not the subscription amount.
  • Confusing customer.subscription.updated webhooks with actual invoice events. The subscription changes immediately, but the invoice is billed later (at the next billing cycle). There's a delay between upgrade and revenue recognition.
  • Assuming all line item increases are expansion. A customer might upgrade their add-on but downgrade their base plan. Calculate the net MRR change across all line items, not just one.

Wrapping Up

Expansion revenue tracking in Stripe requires linking subscription changes to invoice amounts and filtering for recurring charges. By monitoring webhooks and comparing consecutive invoices, you can measure how much existing customers are growing. If you want to track this automatically across tools, Product Analyst can help.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free