Expansion revenue—when existing customers increase their spend through upgrades or feature additions—is critical to track, but Stripe doesn't break it out in your dashboard. You need to listen to subscription changes in real time and calculate the MRR delta yourself to understand how much revenue is coming from customer growth versus new logos.
Detect Subscription Changes with Webhooks
The foundation of expansion tracking is capturing every time a customer's subscription changes. Stripe's customer.subscription.updated webhook event fires whenever someone upgrades, adds seats, or changes plans.
Create a webhook endpoint to receive subscription events
Set up an HTTPS endpoint in your application that Stripe can POST to. This is where you'll receive real-time updates when subscriptions change. You'll need to pass the raw request body to verify the webhook signature with Stripe.
const express = require('express');
const stripe = require('stripe')('sk_live_...');
const app = express();
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec_...'
);
if (event.type === 'customer.subscription.updated') {
handleSubscriptionUpdate(event.data.object);
}
res.json({received: true});
} catch (err) {
res.status(400).send(`Webhook error: ${err.message}`);
}
});
app.listen(3000, () => console.log('Webhook listening'));Register the webhook endpoint in your Stripe Dashboard
Go to Developers > Webhooks in your Stripe Dashboard. Click Add endpoint and paste your webhook URL. Select the customer.subscription.updated event to subscribe to. Copy the webhook signing secret and store it as an environment variable.
// Use the signing secret from your Dashboard
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
const event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
webhookSecret
);express.raw() middleware to capture the raw request body. If you parse JSON first, the signature verification will fail.Extract Expansion Data from Subscription Changes
When a subscription updates, the webhook includes both the new subscription state and the previous_attributes object showing what changed. Use this to calculate whether the customer expanded, contracted, or stayed flat.
Calculate the MRR delta from previous attributes
The customer.subscription.updated event includes a previous_attributes field with the old values. Compare the current amount to the previous amount to see if the customer paid more. For multi-item subscriptions, loop through the items to find net changes.
function handleSubscriptionUpdate(subscription, previousAttributes) {
// Get the current amount from the subscription
const currentAmount = subscription.items.data.reduce((sum, item) => {
return sum + item.price.unit_amount;
}, 0);
// Calculate previous amount
let previousAmount = 0;
if (previousAttributes?.items?.data) {
previousAmount = previousAttributes.items.data.reduce((sum, item) => {
return sum + item.price.unit_amount;
}, 0);
}
const delta = currentAmount - previousAmount;
const isExpansion = delta > 0;
if (isExpansion) {
console.log(`Expansion: $${delta / 100} from ${subscription.customer}`);
logExpansionEvent(subscription.customer, delta);
}
}Handle seat-based and usage-based expansion
For subscriptions with multiple items or metered billing, sum up all changes across the entire subscription. If a customer adds a seat tier and upgrades their base plan, both items contribute to the delta. Check the full items array, not just the first item.
function calculateTotalExpansion(subscription, previousAttributes) {
// Current total from all items
const currentMRR = subscription.items.data.reduce((sum, item) => {
return sum + (item.price.unit_amount || 0);
}, 0);
// Previous total from all items
let previousMRR = 0;
if (previousAttributes?.items?.data) {
previousMRR = previousAttributes.items.data.reduce((sum, item) => {
return sum + (item.price.unit_amount || 0);
}, 0);
}
return currentMRR - previousMRR;
}Filter out downgrades and churn to isolate expansion
Not every subscription change is expansion—customers downgrade, switch plans, or cancel. Only count positive deltas as expansion. Store these separately so your expansion metric stays pure and comparable month over month.
async function logExpansionEvent(customerId, delta, subscriptionId) {
// Only log if it's actually expansion (delta > 0)
if (delta > 0) {
await db.expansionEvents.create({
customer_id: customerId,
mrr_increase: delta,
subscription_id: subscriptionId,
event_date: new Date(),
revenue_cents: delta
});
}
}Query Historical Expansion and Build Reports
Once you're capturing expansion events, use Stripe's API to backfill historical data and create reports. You don't have to rely on webhooks alone—you can query subscription history at any time.
Retrieve a customer's subscription history
Use the Stripe API to list all invoices or subscriptions for a customer. This gives you the complete billing history, which you can use to calculate historical expansion by comparing consecutive invoice amounts.
const stripe = require('stripe')('sk_live_...');
async function getCustomerExpansion(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;
const delta = currentAmount - previousAmount;
if (delta > 0) {
totalExpansion += delta;
}
previousAmount = currentAmount;
});
return totalExpansion;
}Group expansion by time period for reporting
Calculate expansion month by month. Use Stripe's timestamp fields to bucket invoices by period. This lets you see trends—whether expansion is accelerating or slowing down over time.
async function getMonthlyExpansion(startDate, endDate) {
const subscriptions = await stripe.subscriptions.list({
created: {
gte: Math.floor(startDate.getTime() / 1000),
lte: Math.floor(endDate.getTime() / 1000)
},
limit: 100
});
const monthlyExpansion = {};
subscriptions.data.forEach(sub => {
const month = new Date(sub.created * 1000).toISOString().slice(0, 7);
const mrr = sub.items.data.reduce((sum, item) => sum + (item.price.unit_amount || 0), 0);
monthlyExpansion[month] = (monthlyExpansion[month] || 0) + mrr;
});
return monthlyExpansion;
}Common Pitfalls
- Skipping webhook signature verification—attackers can send fake events if you don't validate the
stripe-signatureheader. - Only looking at the current subscription amount instead of the delta—you'll count all revenue, not just the expansion portion.
- Trying to calculate expansion from invoices alone—a new invoice doesn't tell you if the customer upgraded or if it's just the monthly billing cycle.
- Not handling multi-item subscriptions—customers can expand by adding seats, features, or tiers without changing their base plan.
Wrapping Up
Expansion revenue tracking in Stripe requires capturing subscription updates via webhooks and calculating the MRR delta yourself. Once you have that data flowing into your database, you can segment customers, measure expansion velocity, and identify which products are driving growth. If you want to track this automatically across tools and connect it to product usage metrics, Product Analyst can help.