6 min read

How to Monitor Expansion Revenue in Stripe

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.

javascript
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'));
Verify the signature to ensure webhooks are actually from Stripe, not a replay attack.

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.

javascript
// 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
);
Keep your signing secret safe—treat it like an API key.
Watch out: You must use 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.

javascript
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);
  }
}
Amounts in Stripe are in cents, so divide by 100 for display.

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.

javascript
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;
}
For subscriptions with multiple tiers or add-ons, sum all items to get the true MRR change.

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.

javascript
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
    });
  }
}
Keep expansion events separate from downgrades for cleaner reporting.
Tip: A plan change updates the items directly, but adding seats might create a new item. Always check what changed by comparing the full items array, not just the first item.

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.

javascript
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;
}
Invoice amounts are in cents. Reverse the array to process oldest to newest.

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.

javascript
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;
}
Stripe returns timestamps as Unix seconds—multiply by 1000 to convert to milliseconds.
Watch out: Stripe doesn't separate expansion from new customers in standard reports. You have to build this yourself or rely on webhook data. Don't try to reverse-engineer it from invoice totals alone—you'll lose the signal between new and expansion revenue.

Common Pitfalls

  • Skipping webhook signature verification—attackers can send fake events if you don't validate the stripe-signature header.
  • 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.

Track these metrics automatically

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

Try Product Analyst — Free