7 min read

How to Set Up Alerts for Net Revenue Retention in Stripe

Net Revenue Retention tells you whether you're actually growing—it accounts for churn and expansion together. In Stripe, you can monitor NRR by querying subscription data, but you need a system that continuously watches your metrics and alerts you when NRR dips. Here's how to build one.

Calculate NRR from Your Stripe Cohort

NRR is the revenue you keep from a cohort of customers after churn and expansion. You'll query Stripe's API to grab subscription amounts from a month, then track which customers stayed and how much they paid in the current month.

Fetch subscriptions from your cohort month

Use the Stripe API to pull all subscriptions created in a specific month. You'll need your API Keys from Developers > API Keys in your dashboard. This is your baseline revenue.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Get all subscriptions created in a specific month
const cohortStart = Math.floor(new Date('2024-01-01').getTime() / 1000);
const cohortEnd = Math.floor(new Date('2024-02-01').getTime() / 1000);

const subscriptions = await stripe.subscriptions.list({
  created: { gte: cohortStart, lt: cohortEnd },
  limit: 100,
  expand: ['data.customer']
});

// Sum the MRR from these subscriptions (unit_amount is in cents)
const cohortMrr = subscriptions.data.reduce((sum, sub) => {
  const amount = sub.items.data[0]?.price?.unit_amount || 0;
  return sum + amount;
}, 0) / 100; // Convert to dollars

console.log(`Cohort MRR: $${cohortMrr}`);
Query a month's subscriptions and sum their monthly recurring revenue

Measure retention and expansion in the current month

Query current subscriptions from those same customers. Compare the revenue they're paying now to what they paid before. This is your retained MRR plus expansion (or minus churn). Divide by baseline cohort MRR to get NRR.

javascript
// Get all current active subscriptions
const currentSubs = await stripe.subscriptions.list({
  status: 'active',
  limit: 100
});

// Filter for customers from your cohort
const cohortCustomerIds = new Set(subscriptions.data.map(s => s.customer));
const currentCohortMrr = currentSubs.data
  .filter(sub => cohortCustomerIds.has(sub.customer))
  .reduce((sum, sub) => {
    const amount = sub.items.data[0]?.price?.unit_amount || 0;
    return sum + amount;
  }, 0) / 100;

// Calculate NRR
const nrr = (currentCohortMrr / cohortMrr) * 100;
console.log(`Net Revenue Retention: ${nrr.toFixed(2)}%`);
Filter current subscriptions to your cohort and calculate NRR percentage
Watch out: Don't mix annual and monthly subscriptions in one cohort. A $1,200/year plan shows as $100 MRR but only updates 12 times per year. Keep them separate or normalize by billing period.

Set Up Webhooks to Detect Changes in Real-Time

Manually calculating NRR once a month won't alert you to problems. Set up a webhook that listens for subscription changes and recalculates NRR whenever something shifts.

Create a webhook endpoint in your backend

Go to Developers > Webhooks in Stripe. Add a new endpoint pointing to your server (e.g., https://yourapp.com/webhooks/stripe). Enable events: customer.subscription.updated, customer.subscription.deleted, and invoice.payment_succeeded.

javascript
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

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

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.error('Webhook signature failed:', err);
    return res.sendStatus(400);
  }

  // Handle subscription changes
  if (event.type === 'customer.subscription.updated') {
    const subscription = event.data.object;
    const previousData = event.data.previous_attributes;

    console.log(`Subscription ${subscription.id} updated for ${subscription.customer}`);
    if (previousData.items) {
      console.log('Amount changed - potential churn or expansion');
    }
  }

  res.sendStatus(200);
});

app.listen(3000);
Express endpoint that verifies Stripe webhook signatures and logs subscription changes

Trigger NRR recalculation when subscriptions change

Inside your webhook handler, queue a job to recalculate NRR for the current cohort. Don't do it synchronously—webhook responses should be fast. Use a job queue (like Bull or AWS SQS) to compute NRR asynchronously, then compare to your threshold.

javascript
async function calculateNRRForCohort(cohortMonth) {
  // Use the same logic from step 1-2 above
  const cohortStart = Math.floor(new Date(cohortMonth).getTime() / 1000);
  const cohortEnd = cohortStart + 2592000; // ~30 days

  const cohortSubs = await stripe.subscriptions.list({
    created: { gte: cohortStart, lt: cohortEnd },
    limit: 100
  });

  const cohortMrr = cohortSubs.data.reduce((sum, sub) => 
    sum + (sub.items.data[0]?.price?.unit_amount || 0), 0) / 100;

  const currentSubs = await stripe.subscriptions.list({ status: 'active', limit: 100 });
  const cohortIds = new Set(cohortSubs.data.map(s => s.customer));
  const retainedMrr = currentSubs.data
    .filter(sub => cohortIds.has(sub.customer))
    .reduce((sum, sub) => sum + (sub.items.data[0]?.price?.unit_amount || 0), 0) / 100;

  const nrr = (retainedMrr / cohortMrr) * 100;
  const threshold = 95; // Your target NRR

  if (nrr < threshold) {
    console.warn(`NRR ALERT: ${nrr.toFixed(2)}% < ${threshold}%`);
    // Trigger alert
  }

  return nrr;
}

// In your webhook handler:
if (event.type === 'customer.subscription.updated') {
  // Queue this for async processing
  jobQueue.add('recalculate-nrr', { cohortMonth: '2024-01' });
}
Recalculate NRR when a subscription changes and compare against threshold
Tip: Don't recalculate NRR on every webhook event. Run this once daily during off-hours instead. You'll avoid unnecessary API calls and keep alert noise down.

Send Alerts When NRR Drops

Once you've detected a threshold breach, notify your revenue team immediately. Integrate with Slack or email to get eyes on the problem fast.

Route alerts to Slack or email

When NRR falls below your target, send a message to your revenue or ops channel. Include the current NRR value, the threshold, and a direct link to your Stripe Dashboard subscriptions tab so the team can drill in quickly.

javascript
const axios = require('axios');

async function alertNRRDrop(nrr, threshold) {
  const slackWebhook = process.env.SLACK_NRR_ALERTS;
  const status = nrr < threshold ? '⚠️ WARNING' : '✅ OK';

  const payload = {
    text: `${status} Net Revenue Retention`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*${status}: NRR = ${nrr.toFixed(2)}%* (target: ${threshold}%)\n\nYour cohort retention dropped. Check for churn spike or failed renewals.`
        }
      },
      {
        type: 'actions',
        elements: [
          {
            type: 'button',
            text: { type: 'plain_text', text: 'View Stripe Dashboard' },
            url: 'https://dashboard.stripe.com/subscriptions',
            action_id: 'view_stripe'
          }
        ]
      }
    ]
  };

  await axios.post(slackWebhook, payload);
  console.log(`NRR alert sent: ${nrr.toFixed(2)}%`);
}

// Call after calculating NRR
await alertNRRDrop(92.5, 95);
Send a Slack notification with NRR metrics and a link to Stripe
Tip: Add a cooldown so you don't spam alerts. Send one message per day even if NRR stays low. Log every alert to a database so you can track trends and tune your threshold over time.

Common Pitfalls

  • Forgetting downgrades are contraction, not churn. A customer who moves from $500/mo to $300/mo is still active in Stripe. Your NRR calculation must include both expansion and contraction to be accurate.
  • Querying NRR on every webhook event will overwhelm your database and Stripe's rate limits. Use a scheduled job (once daily) instead of real-time calculation.
  • Mixing subscriptions with different billing cycles. A monthly sub and annual sub have different MRR calculations. Create separate cohorts or normalize by billing period.
  • Ignoring failed payments. Stripe marks subscriptions as 'past_due' or 'unpaid' but they're still 'active' until the retry window closes. Exclude these from NRR until they actually churn.

Wrapping Up

Net Revenue Retention in Stripe requires querying cohort subscriptions, measuring retention and expansion, and setting up alerts when the metric drops. Start with a daily scheduled job, wire up Slack notifications, and log every calculation so you can spot trends. If you want to track this automatically across tools, Product Analyst can help.

Track these metrics automatically

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

Try Product Analyst — Free