7 min read

How to Track Subscription Management in Stripe

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.

javascript
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});
});
Always verify the webhook signature to ensure it came from Stripe

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.

javascript
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.

javascript
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)
  });
}
Watch out: Stripe retries failed webhooks. Use the 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?'

javascript
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.

javascript
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.

javascript
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;
}
Tip: Use the 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.

javascript
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.

javascript
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-signature header, anyone can POST to your endpoint. Always use stripe.webhooks.constructEvent().
  • Double-counting revenue from duplicate webhook deliveries. Stripe retries failed webhooks, so the same event can arrive twice. Use event.id as a unique key.
  • Treating past_due the same as canceled. In Stripe, past_due subscriptions are still retrying payment and generating revenue. Only paused and canceled have 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.

Track these metrics automatically

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

Try Product Analyst — Free