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.
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;
}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.
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']);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.
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.
// 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.
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 });
});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.
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.
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),
});
}Common Pitfalls
- Counting trial subscriptions toward MRR — Stripe separates
trial_endfromcurrent_period_end. Filter bystatus: 'active'and ignore subscriptions wheretrial_endis 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.