Tracking subscription changes in Stripe matters because your most critical business metrics live here—MRR, churn, expansion revenue. But subscription data is scattered across events, API calls, and your billing system. You need to know when subscriptions are created, updated, paused, or canceled, and tie those changes to revenue impact.
Set Up Webhook Listeners for Subscription Events
Webhooks are Stripe's real-time notification system. Instead of polling the API every minute, Stripe pushes subscription events to you instantly.
Create a webhook endpoint in the Stripe Dashboard
In the Stripe Dashboard, go to Developers > Webhooks and click Add Endpoint. Point it to your server (e.g., https://yourapp.com/webhooks/stripe). Select the subscription events you care about: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, and customer.subscription.paused_will_be_canceled.
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
res.json({received: true});
});Handle subscription events in your code
Listen for specific event types and log subscription changes. Use the data.object property to access subscription details. For customer.subscription.updated, you can track what changed by comparing the old and new states.
switch(event.type) {
case 'customer.subscription.created':
const newSub = event.data.object;
console.log(`Sub created: ${newSub.id}, customer: ${newSub.customer}`);
saveToDatabase(newSub);
break;
case 'customer.subscription.updated':
const updatedSub = event.data.object;
console.log(`Sub updated: ${updatedSub.id}, status: ${updatedSub.status}`);
// Track price changes, quantity changes, status transitions
break;
case 'customer.subscription.deleted':
const canceledSub = event.data.object;
console.log(`Sub canceled: ${canceledSub.id}`);
console.log(`Reason: ${canceledSub.cancellation_details?.reason}`);
break;
}Store subscription events for later analysis
Don't rely on webhook logs alone. Save subscription events to your database or data warehouse so you can query subscription history. Store the subscription ID, customer ID, event type, amount, status, and timestamp.
async function logSubscriptionEvent(event) {
const subscription = event.data.object;
await database.subscriptionEvents.create({
event_id: event.id,
subscription_id: subscription.id,
customer_id: subscription.customer,
event_type: event.type,
amount: subscription.items.data[0].price.unit_amount,
currency: subscription.currency,
status: subscription.status,
timestamp: new Date(event.created * 1000)
});
}event.id as a unique key to prevent saving duplicate records.Query Subscription Data with the Stripe API
Webhooks are event-driven, but sometimes you need to query subscription data directly—to audit a customer's history, calculate MRR, or check current subscription status.
List subscriptions with filters
Use stripe.subscriptions.list() to fetch subscriptions. Filter by status (active, past_due, paused, canceled) or customer to answer specific questions like 'How many active subscriptions do we have?' or 'What's customer X's subscription history?'
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Get all active subscriptions
const activeSubscriptions = await stripe.subscriptions.list({
status: 'active',
limit: 100
});
// Get subscriptions for a specific customer
const customerSubscriptions = await stripe.subscriptions.list({
customer: 'cus_ABC123',
limit: 100
});
// Get subscriptions with a specific price
const enterpriseSubs = await stripe.subscriptions.list({
price: 'price_XYZ789',
limit: 100
});Calculate Monthly Recurring Revenue (MRR)
Sum all active subscription amounts to get your MRR. Account for billing intervals—annual prices should be divided by 12. Account for quantity too, in case a customer has multiple units of the same plan.
async function calculateMRR() {
const subscriptions = await stripe.subscriptions.list({
status: 'active',
limit: 100
});
let totalMRR = 0;
subscriptions.data.forEach(sub => {
sub.items.data.forEach(item => {
const interval = item.price.recurring.interval;
const unitAmount = item.price.unit_amount / 100; // Convert cents to dollars
const quantity = item.quantity || 1;
// Normalize annual subscriptions to monthly
const monthlyAmount = interval === 'year' ? unitAmount / 12 : unitAmount;
totalMRR += monthlyAmount * quantity;
});
});
return totalMRR;
}Track subscription churn rate
Query your subscription events table to count how many subscriptions were canceled in a time period. Divide by total active subscriptions at the start of the period to get your churn rate.
async function getChurnRate(startDate, endDate) {
// Count subscriptions canceled in this period
const canceledSubs = await database.subscriptionEvents.findMany({
where: {
event_type: 'customer.subscription.deleted',
timestamp: {
gte: startDate,
lte: endDate
}
}
});
const churnedCount = new Set(
canceledSubs.map(e => e.subscription_id)
).size;
// Count total active subscriptions at start of period
const activeAtStart = await database.subscriptionEvents.count({
where: {
event_type: 'customer.subscription.created',
timestamp: { lte: startDate }
},
distinct: ['subscription_id']
});
return (churnedCount / activeAtStart) * 100;
}expand parameter to get related customer data in one call instead of making separate requests: stripe.subscriptions.list({ expand: ['data.customer'] })Monitor Subscription Health Over Time
Track trends in your subscription base. Which cohorts are healthy, where are you losing revenue, and which pricing plan changes created problems.
Segment by plan and track downgrades
Group subscriptions by plan to see which tiers are losing customers. Use customer.subscription.updated events where the price changed to identify downgrades (moving to a cheaper plan) vs. upgrades.
async function trackDowngrades(startDate, endDate) {
const updates = await database.subscriptionEvents.findMany({
where: {
event_type: 'customer.subscription.updated',
timestamp: { gte: startDate, lte: endDate }
}
});
// You'll need to compare old vs new price_id from your events
const downgrades = updates.filter(e => {
const oldPrice = e.previous_price_amount;
const newPrice = e.new_price_amount;
return newPrice < oldPrice;
});
const downgradesByPlan = {};
downgrades.forEach(d => {
const plan = d.new_plan_name;
downgradesByPlan[plan] = (downgradesByPlan[plan] || 0) + 1;
});
return downgradesByPlan;
}Set up revenue loss alerts
Monitor for unexpected MRR drops or spikes in high-value subscription cancellations. When MRR falls outside your expected range, alert your team so you can investigate immediately.
async function checkRevenueAlerts() {
const currentMRR = await calculateMRR();
const previousMRR = await database.metrics.findFirst({
orderBy: { created_at: 'desc' }
})?.mrr;
if (!previousMRR) return;
const percentChange = ((currentMRR - previousMRR) / previousMRR) * 100;
// Alert if MRR drops more than 5%
if (percentChange < -5) {
console.error(`⚠️ MRR dropped ${Math.abs(percentChange).toFixed(1)}% — investigate churn`);
}
// Alert if high-value subscription (>$5000/month) canceled
const recentCancels = await stripe.subscriptions.list({
status: 'canceled',
limit: 50
});
recentCancels.data.forEach(sub => {
const amount = sub.items.data[0].price.unit_amount / 100;
if (amount > 5000) {
console.error(`⚠️ High-value customer canceled: $${amount}/month`);
}
});
}Common Pitfalls
- Forgetting to verify webhook signatures. If you don't validate the
stripe-signatureheader, anyone can POST to your endpoint. Always usestripe.webhooks.constructEvent(). - Double-counting revenue from duplicate webhook deliveries. Stripe retries failed webhooks, so the same event can arrive twice. Use
event.idas a unique key. - Treating
past_duethe same ascanceled. In Stripe,past_duesubscriptions are still retrying payment and generating revenue. Onlypausedandcanceledhave stopped paying. - Ignoring
cancellation_details.reason. When subscriptions cancel, Stripe logs whether it was customer-requested, dunning failure, or admin-initiated. Use this to segment churn by root cause.
Wrapping Up
You now have real-time visibility into your subscription lifecycle. You're capturing every state change, calculating MRR and churn, and spotting revenue problems before they become disasters. If you want to track this automatically across Stripe, Amplitude, PostHog, and other tools in one central dashboard, Product Analyst can help.