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.
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'
}
});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.
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});
});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.
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.
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;
};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.
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.
// 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'
);
}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_paidstays flat. Check the invoice itself, not the subscription amount. - Confusing
customer.subscription.updatedwebhooks 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.