If you're running a SaaS business, NRR tells you whether your existing customers are expanding or shrinking. Stripe doesn't calculate this for you—you need to extract subscription data and do the math yourself. We'll walk through querying your subscriptions, tracking changes, and computing your NRR.
Pull Your Baseline and Period MRR
Start by fetching all active subscriptions and calculating your starting MRR for the period you're analyzing.
Query all active subscriptions from Stripe
Use the Subscriptions API to get every active sub. Filter by status to exclude canceled or past-due subscriptions. You'll need your Stripe API key.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const startOfMonth = new Date(2025, 2, 1); // March 1st
const endOfMonth = new Date(2025, 3, 1); // April 1st
// Get all active subscriptions
const subscriptions = await stripe.subscriptions.list({
status: 'active',
limit: 100 // paginate if you have more
});
console.log(`Found ${subscriptions.data.length} active subscriptions`);Calculate your baseline MRR
Sum the amount from each subscription's plan. Divide by 100 to convert from cents. This is your starting MRR for the period.
// Calculate MRR from subscriptions
const calculateMRR = (subscriptions) => {
return subscriptions.data.reduce((total, subscription) => {
const items = subscription.items.data;
if (!items || items.length === 0) return total;
const amount = items.reduce((sum, item) => {
return sum + (item.plan.amount || 0);
}, 0);
return total + (amount / 100); // Convert cents to dollars
}, 0);
};
const startingMRR = calculateMRR(subscriptions);
console.log(`Starting MRR: $${startingMRR.toFixed(2)}`);subscription.items. For pure subscription revenue, focus on the plan's base amount.Track Expansion and Churn During the Period
NRR = (Starting MRR + Expansion − Churn) / Starting MRR. You need to count which subscriptions expanded, and which churned or downgraded.
Find subscriptions canceled during the period
Query for subscriptions with status: 'canceled' created during your month. This is your churn. Don't include downgrades here—track those separately in the next step.
// Get canceled subscriptions
const canceledSubscriptions = await stripe.subscriptions.list({
status: 'canceled',
created: {
gte: Math.floor(startOfMonth.getTime() / 1000),
lt: Math.floor(endOfMonth.getTime() / 1000)
},
limit: 100
});
const churnMRR = canceledSubscriptions.data.reduce((total, sub) => {
return total + (sub.items.data[0]?.plan.amount || 0) / 100;
}, 0);
console.log(`Churn MRR: $${churnMRR.toFixed(2)}`);Calculate expansion and contraction from plan changes
Query the Events API for customer.subscription.updated events. Compare the previous_attributes.items with the current items.data to find upgrades (expansion) and downgrades (contraction).
// Get subscription update events
const events = await stripe.events.list({
type: 'customer.subscription.updated',
created: {
gte: Math.floor(startOfMonth.getTime() / 1000),
lt: Math.floor(endOfMonth.getTime() / 1000)
},
limit: 100
});
let expansionMRR = 0;
let contractionMRR = 0;
for (const event of events.data) {
const { previous_attributes } = event.data;
if (!previous_attributes || !previous_attributes.items) continue;
const oldAmount = previous_attributes.items.data[0]?.plan.amount || 0;
const newAmount = event.data.object.items.data[0]?.plan.amount || 0;
const delta = (newAmount - oldAmount) / 100;
if (delta > 0) {
expansionMRR += delta;
} else if (delta < 0) {
contractionMRR += Math.abs(delta);
}
}
console.log(`Expansion MRR: $${expansionMRR.toFixed(2)}`);
console.log(`Contraction MRR: $${contractionMRR.toFixed(2)}`);customer.subscription.updated events include a previous_attributes field showing what changed. This is your source of truth for upsells and downgrades.Calculate Your NRR
Apply the NRR formula
NRR = (Starting MRR + Expansion − Churn − Contraction) / Starting MRR × 100. An NRR above 100% means existing customers are expanding net; below 100% means they're shrinking. New customer MRR is separate—don't include it.
// Calculate NRR
const netRevenuRetention = (
(startingMRR + expansionMRR - contractionMRR - churnMRR) / startingMRR
) * 100;
console.log(`\nNRR Calculation:`);
console.log(`Starting MRR: $${startingMRR.toFixed(2)}`);
console.log(`Expansion: +$${expansionMRR.toFixed(2)}`);
console.log(`Contraction: -$${contractionMRR.toFixed(2)}`);
console.log(`Churn: -$${churnMRR.toFixed(2)}`);
console.log(`Ending MRR (existing): $${(startingMRR + expansionMRR - contractionMRR - churnMRR).toFixed(2)}`);
console.log(`\nNRR: ${netRevenuRetention.toFixed(1)}%`);
if (netRevenuRetention >= 100) {
console.log('✓ Growing revenue from existing customers');
} else {
console.log('⚠ Losing revenue from existing customers');
}Common Pitfalls
- Forgetting to exclude new customer MRR from the NRR calculation. NRR only measures existing customer changes, not new business.
- Using
subscription.createdfor churn instead ofcanceled_at. A subscription canceled mid-month may have been created months earlier—track when it left, not when it started. - Counting churn twice if a customer downgrades and then cancels. The
customer.subscription.updatedevent captures the downgrade; the cancellation is separate. Track the final state, not the full history. - Assuming
previous_attributesalways exists incustomer.subscription.updatedevents. If only metadata or description changed, the plan data won't be there—check before accessing.
Wrapping Up
Once you have your NRR, you can track it month-over-month to spot trends in customer health. This calculation lives outside Stripe—you're aggregating raw subscription data and doing math—so consider automating it or plugging your Stripe data into a data warehouse. If you want to track NRR and other metrics automatically across tools, Product Analyst can help.