5 min read

How to Set Up Alerts for MRR in Stripe

Your MRR is your business's heartbeat. Most subscription businesses check it manually, refreshing the Stripe Dashboard obsessively. Stripe doesn't have a native MRR alert, but you can build one in an afternoon using webhooks and the Subscriptions API. This approach catches churn, upgrades, and downgrades in real-time.

Calculate MRR from Your Subscriptions

MRR in Stripe is straightforward: sum all active subscription prices. The tricky part is handling pagination and making sure you don't count trials or canceled subscriptions.

Query active subscriptions with the Stripe API

Use stripe.subscriptions.list() filtered by status: 'active'. The API returns paginated results, so you'll loop through batches using starting_after. For each subscription, sum the unit_amount of each price object in the items.data array. Convert from cents to dollars at the end.

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

async function calculateMRR() {
  let mrrCents = 0;
  let hasMore = true;
  let startingAfter = null;

  while (hasMore) {
    const subscriptions = await stripe.subscriptions.list({
      status: 'active',
      limit: 100,
      starting_after: startingAfter,
    });

    for (const sub of subscriptions.data) {
      for (const item of sub.items.data) {
        if (item.price.recurring && item.price.recurring.interval === 'month') {
          mrrCents += item.price.unit_amount * item.quantity;
        }
      }
    }

    hasMore = subscriptions.has_more;
    if (hasMore) {
      startingAfter = subscriptions.data[subscriptions.data.length - 1].id;
    }
  }

  return mrrCents / 100;
}
Iterate through all paginated results and sum recurring monthly charges.

Store the baseline MRR with a timestamp

Save this calculation to a database table with a timestamp. This becomes your reference point. When webhooks fire, you'll recalculate and compare the new MRR against this baseline. Include the subscription count and any breakdown you care about.

javascript
const currentMRR = await calculateMRR();

// Save to your database (example using a simple object store)
const baseline = {
  mrrAmount: Math.round(currentMRR * 100), // Store in cents
  timestamp: new Date(),
  calculatedAt: new Date().toISOString(),
};

await db.table('mrr_baseline').upsert(baseline, ['id']);
Watch out: Annual subscriptions inflate MRR if you count them as-is. Normalize yearly prices by dividing by 12. Also, ignore subscriptions with trial_end dates in the future — they won't generate revenue yet.

Set Up Webhooks to Catch Subscription Events

Webhooks fire instantly when subscriptions change. Instead of polling Stripe every minute, you respond to events as they happen. Four events matter: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, and invoice.payment_succeeded.

Create a webhook endpoint in your application

In your server code (Node.js, Python, etc.), create an endpoint that receives POST requests from Stripe. Stripe will send a signed payload. Use stripe.webhooks.constructEvent() to verify the signature before processing. This prevents spoofed requests.

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

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

    let event;
    try {
      event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
    } catch (err) {
      console.error(`Webhook signature verification failed: ${err.message}`);
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    // Handle the event
    console.log(`Received event: ${event.type}`);
    res.json({ received: true });
  }
);

Enable webhooks in the Stripe Dashboard

Go to Developers > Webhooks in the Stripe Dashboard. Click Add endpoint. Enter your webhook URL (e.g., https://yourapp.com/webhooks/stripe). Under Select events to send, enable the subscription and invoice events. Copy the signing secret and save it to your .env file as STRIPE_WEBHOOK_SECRET.

javascript
// After Stripe sends your webhook secret, verify it's set
if (!process.env.STRIPE_WEBHOOK_SECRET) {
  throw new Error('STRIPE_WEBHOOK_SECRET is not set in environment variables');
}

// Start listening
app.listen(3000, () => {
  console.log('Webhook endpoint listening on port 3000');
});

Recalculate MRR when subscriptions change

Inside your webhook handler, trigger an MRR recalculation. Compare the new MRR against your baseline. If it differs by more than your threshold, log it and send an alert. Update your baseline for the next comparison.

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

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // React to subscription changes
  if (['customer.subscription.created', 'customer.subscription.updated', 'customer.subscription.deleted'].includes(event.type)) {
    const newMRR = await calculateMRR();
    const baseline = await db.table('mrr_baseline').select('*').first();
    const change = newMRR - baseline.mrrAmount / 100;
    const changePercent = Math.abs(change) / (baseline.mrrAmount / 100) * 100;

    // Log the change
    await db.table('mrr_changes').insert({
      oldMRR: baseline.mrrAmount / 100,
      newMRR,
      change,
      changePercent,
      eventType: event.type,
      timestamp: new Date(),
    });

    // Update baseline
    await db.table('mrr_baseline').update({
      mrrAmount: Math.round(newMRR * 100),
      timestamp: new Date(),
    });
  }

  res.json({ received: true });
});
Tip: Webhooks can arrive out of order or duplicate. Use a uniqueness constraint on your mrr_changes table keyed by event ID to prevent double-processing.

Send Alerts on Significant MRR Changes

Alerts are useless if they don't reach you. Set thresholds that actually matter to your business, then deliver notifications to Slack, email, or your monitoring tool.

Define meaningful thresholds

Not every MRR change deserves an alert. A 1% decrease is normal churn. A 20% drop in a day is an emergency. Define thresholds in absolute dollars (e.g., $500 change) or percentage terms (e.g., 5% change), depending on your business size.

javascript
const MRR_ALERT_CONFIG = {
  criticalDropPercent: 10, // Alert if MRR drops 10% or more
  criticalGainPercent: 15, // Alert if MRR jumps 15% or more
  minChangeAmount: 50000, // Alert only if change is >= $500
};

async function shouldAlert(oldMRR, newMRR) {
  const change = newMRR - oldMRR;
  const changePercent = Math.abs(change) / oldMRR * 100;

  if (Math.abs(change) < MRR_ALERT_CONFIG.minChangeAmount) {
    return false; // Ignore noise
  }

  if (change < 0 && changePercent >= MRR_ALERT_CONFIG.criticalDropPercent) {
    return true; // Critical drop
  }

  if (change > 0 && changePercent >= MRR_ALERT_CONFIG.criticalGainPercent) {
    return true; // Unexpected spike
  }

  return false;
}

Send alerts to Slack or email

Use Slack's Incoming Webhooks for instant notifications, or send email via a service like Resend. Include the old MRR, new MRR, the change amount, and the percentage change. Make the alert actionable — link to the Stripe Dashboard or your internal dashboard.

javascript
async function sendMRRAlert(oldMRR, newMRR, changePercent) {
  const change = newMRR - oldMRR;
  const emoji = change > 0 ? '📈' : '📉';
  const slackWebhook = process.env.SLACK_WEBHOOK_URL;

  const payload = {
    text: `${emoji} MRR Alert: ${changePercent.toFixed(1)}% change`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*MRR Change Detected*\n` +
            `Previous: $${oldMRR.toFixed(2)}\n` +
            `Current: $${newMRR.toFixed(2)}\n` +
            `Change: ${change > 0 ? '+' : ''}$${change.toFixed(2)} (${changePercent.toFixed(1)}%)`,
        },
      },
      {
        type: 'actions',
        elements: [
          {
            type: 'button',
            text: { type: 'plain_text', text: 'View in Stripe' },
            url: 'https://dashboard.stripe.com/subscriptions',
          },
        ],
      },
    ],
  };

  await fetch(slackWebhook, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });
}
Watch out: Threshold tuning takes time. Start with conservative thresholds (10% or $1000 change), then loosen them as you learn your baseline churn patterns.

Common Pitfalls

  • Counting trial subscriptions toward MRR — Stripe separates trial_end from current_period_end. Filter by status: 'active' and ignore subscriptions where trial_end is in the future.
  • Not verifying webhook signatures — anyone can POST to your endpoint. Always call stripe.webhooks.constructEvent() with your signing secret. Skipping this is a security vulnerability.
  • Ignoring timezone mismatches — Stripe returns all timestamps in UTC. If your database is in a different timezone, baseline comparisons will drift. Use UTC everywhere.
  • Setting alert thresholds too low — you'll get alert fatigue from daily pricing fluctuations. Start with 5% or higher, or set a minimum dollar threshold ($500+) to filter noise.

Wrapping Up

You now have a real-time MRR alert system using Stripe webhooks and the Subscriptions API. Tune your thresholds to match your business — whether that's 2% or 20%. If you want to track this automatically across tools and integrate with your billing data, Product Analyst can help.

Track these metrics automatically

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

Try Product Analyst — Free