6 min read

How to Monitor Net Revenue Retention in Stripe

Net Revenue Retention measures whether you're growing revenue from existing customers—the difference between expansion and churn. Stripe doesn't calculate NRR for you, but it gives you everything you need: subscription data, invoice history, and change events. You'll need to pull this data, aggregate it monthly, and compute the metric yourself.

Fetch Your Subscription Baseline

Start by understanding your current recurring revenue base. You'll pull all active subscriptions and sum their monthly amounts.

List all active subscriptions

Use the stripe.subscriptions.list() method to fetch subscriptions. Filter by status: 'active' to exclude canceled and past_due. This gives you your current customer base. You'll want to paginate through results if you have thousands of customers—Stripe limits responses to 100 items per request.

javascript
const stripe = require('stripe')('sk_live_...');

let allSubscriptions = [];
let startingAfter = null;
let hasMore = true;

while (hasMore) {
  const batch = await stripe.subscriptions.list({
    status: 'active',
    limit: 100,
    ...(startingAfter && { starting_after: startingAfter })
  });
  
  allSubscriptions = allSubscriptions.concat(batch.data);
  startingAfter = batch.data[batch.data.length - 1]?.id;
  hasMore = batch.has_more;
}

console.log(`Found ${allSubscriptions.length} active subscriptions`);
Fetch all active subscriptions with pagination

Calculate Monthly Recurring Revenue

Each subscription has items with recurring pricing. Sum the amount of each plan—this is your MRR base. Note that amounts are in cents, so divide by 100 for the actual currency value. Ignore one-time charges; they're marked with recurring: null.

javascript
let mrr = 0;

allSubscriptions.forEach(subscription => {
  subscription.items.data.forEach(item => {
    if (item.price.recurring) {
      const monthlyAmount = item.price.unit_amount / 100;
      const quantity = item.quantity || 1;
      mrr += monthlyAmount * quantity;
    }
  });
});

console.log(`Current MRR: $${mrr.toFixed(2)}`);
// This is your beginning-of-month baseline
Sum monthly revenue from all subscriptions

Monitor Subscription Changes via Events

Subscriptions change constantly—upgrades, downgrades, cancellations. Use Stripe's Events API to capture these in real time.

Set up a webhook for subscription events

Create an endpoint that receives customer.subscription.updated and customer.subscription.deleted events. You can test locally using the Stripe CLI with stripe listen --forward-to localhost:3000/webhook. In production, register your endpoint in the Stripe Dashboard > Developers > Webhooks.

javascript
const express = require('express');
const app = express();

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  const event = stripe.webhooks.constructEvent(
    req.body,
    sig,
    'whsec_test_...'
  );

  if (event.type === 'customer.subscription.updated') {
    const subscription = event.data.object;
    const previousAttributes = event.data.previous_attributes;
    
    console.log(`Subscription ${subscription.id} changed`);
    if (previousAttributes?.items) {
      console.log('Items modified - upgrade or downgrade');
    }
  }
  
  if (event.type === 'customer.subscription.deleted') {
    const subscription = event.data.object;
    console.log(`Subscription ${subscription.id} canceled`);
  }
  
  res.json({received: true});
});
Webhook handler for subscription changes

Calculate revenue delta from each event

When you receive an update event, compute the old and new MRR from that subscription. The difference is expansion (positive) or contraction (negative). For deletions, the revenue delta is the full subscription amount going to churn. Store these deltas with timestamps so you can aggregate by month.

javascript
function calculateMRRDelta(subscription, previousAttributes) {
  let oldMRR = 0;
  let newMRR = 0;

  // Calculate new MRR
  subscription.items.data.forEach(item => {
    if (item.price.recurring) {
      newMRR += (item.price.unit_amount / 100) * (item.quantity || 1);
    }
  });

  // Calculate old MRR from previous state
  if (previousAttributes?.items?.data) {
    previousAttributes.items.data.forEach(item => {
      if (item.price?.recurring) {
        oldMRR += (item.price.unit_amount / 100) * (item.quantity || 1);
      }
    });
  }

  const delta = newMRR - oldMRR;
  return {
    oldMRR,
    newMRR,
    delta,
    type: delta > 0 ? 'expansion' : delta < 0 ? 'contraction' : 'no_change'
  };
}

const change = calculateMRRDelta(subscription, event.data.previous_attributes);
console.log(`Customer: ${change.type} of $${Math.abs(change.delta).toFixed(2)}`);
// Save to database with timestamp for monthly aggregation
Detect expansion vs. contraction from each change
Watch out: customer.subscription.updated events fire for any subscription change, including metadata edits that don't affect revenue. Always check previous_attributes to confirm what actually changed before counting it.

Aggregate Monthly Metrics and Compute NRR

Sum all the changes by month to get your NRR.

Group changes by month

For each month, sum expansion, contraction, and churn separately. This gives you the components of NRR. Include involuntary churn by also monitoring invoice.payment_failed events—customers who stop paying before they cancel should be counted as churn.

javascript
// Monthly aggregation
const monthlyMetrics = {};

function addMetricChange(date, type, amount) {
  const monthKey = date.toISOString().slice(0, 7); // '2026-03'
  
  if (!monthlyMetrics[monthKey]) {
    monthlyMetrics[monthKey] = {
      expansion: 0,
      contraction: 0,
      churn: 0
    };
  }
  
  if (type === 'expansion') {
    monthlyMetrics[monthKey].expansion += amount;
  } else if (type === 'contraction') {
    monthlyMetrics[monthKey].contraction += amount;
  } else if (type === 'churn') {
    monthlyMetrics[monthKey].churn += amount;
  }
}

// Calculate NRR: (Beginning MRR + Expansion - Contraction - Churn) / Beginning MRR
function calculateNRR(monthKey, beginningMRR) {
  const metrics = monthlyMetrics[monthKey];
  const netRevenue = beginningMRR + metrics.expansion - metrics.contraction - metrics.churn;
  return (netRevenue / beginningMRR) * 100;
}

console.log(`March NRR: ${calculateNRR('2026-03', 50000).toFixed(1)}%`);
Aggregate monthly changes and compute NRR
Tip: NRR above 100% means you're expanding faster than losing revenue—healthy growth. Below 100% signals churn or downgrades are eroding your base. Most B2B SaaS targets 120%+ for strong momentum.

Common Pitfalls

  • Counting test mode subscriptions in production MRR. Always use production keys when calculating real revenue, and filter out any test_clock subscriptions from your aggregations.
  • Including one-time charges in recurring revenue. Verify price.recurring is truthy before adding an item's amount to MRR—one-time items will have recurring: null.
  • Missing involuntary churn. Customers can churn due to failed payments long before they cancel manually. Monitor invoice.payment_failed and track unpaid subscription events separately from voluntary cancellations.
  • Timezone misalignment causing off-by-one errors. Stripe uses UTC; convert created and period_start timestamps to your local timezone before grouping by calendar month to avoid late-month events being assigned to the wrong month.

Wrapping Up

Monitoring NRR in Stripe requires pulling subscription data, tracking changes through events, and aggregating monthly—it's not built into the Dashboard. You now have a system to capture expansion, contraction, and churn. If you want to track this automatically across tools and sync metrics to your data warehouse, Product Analyst can help.

Track these metrics automatically

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

Try Product Analyst — Free