Stripe doesn't surface churn rate directly, so you need to calculate it from subscription data. The key is knowing where to pull the numbers and understanding what counts as a churned customer. Let's walk through it.
Extract Subscription Data from Stripe
The Stripe API gives you all the raw subscription events. You pull them, filter by date and status, then count.
Fetch subscriptions in a date range
Use the stripe.subscriptions.list() method with a created filter to grab all subscriptions created or updated during your measurement window. Include the status: 'all' parameter so you get active, canceled, and everything else.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const subscriptions = await stripe.subscriptions.list({
limit: 100,
status: 'all',
created: {
gte: Math.floor(new Date('2026-03-01').getTime() / 1000),
lt: Math.floor(new Date('2026-04-01').getTime() / 1000)
}
});Handle pagination for large customer bases
The API returns 100 results by default. If you have more subscriptions, loop through pages using the starting_after cursor.
let allSubscriptions = [];
let hasMore = true;
let startingAfter = null;
while (hasMore) {
const batch = await stripe.subscriptions.list({
limit: 100,
status: 'all',
starting_after: startingAfter,
created: { gte: startTimestamp, lt: endTimestamp }
});
allSubscriptions = allSubscriptions.concat(batch.data);
hasMore = batch.has_more;
startingAfter = batch.data[batch.data.length - 1]?.id;
}created timestamp is when the subscription was created, not when it was canceled. For canceled subscriptions, use canceled_at or the customer.subscription.deleted webhook event instead.Calculate Churn Rate from the Data
Now that you have the subscription list, separate canceled from active and compute the rate.
Count active and canceled subscriptions
Filter subscriptions by status. active means the customer has a live subscription; canceled means they've churned. Divide canceled by your baseline to get the churn rate.
const active = allSubscriptions.filter(sub => sub.status === 'active').length;
const canceled = allSubscriptions.filter(sub => sub.status === 'canceled').length;
const churnRate = (canceled / (active + canceled)) * 100;
console.log(`Active: ${active}, Canceled: ${canceled}, Churn Rate: ${churnRate.toFixed(2)}%`);Segment by subscription plan
Churn often varies by plan tier. Filter by items.data[0].price.id to see which plans have the highest churn and identify your most at-risk segments.
const churnByPlan = {};
allSubscriptions.forEach(sub => {
const planId = sub.items.data[0]?.price.id;
if (!churnByPlan[planId]) churnByPlan[planId] = { active: 0, canceled: 0 };
if (sub.status === 'active') churnByPlan[planId].active++;
if (sub.status === 'canceled') churnByPlan[planId].canceled++;
});
Object.entries(churnByPlan).forEach(([plan, counts]) => {
const rate = (counts.canceled / (counts.active + counts.canceled)) * 100;
console.log(`Plan ${plan}: ${rate.toFixed(2)}% churn`);
});sub.canceled_at to filter for cancellations in a specific window. This is more accurate than relying on the subscription's creation date.Automate Churn Monitoring with Webhooks
Running manual queries every week gets stale. Listen to Stripe's webhook events and log churn as it happens.
Listen for the customer.subscription.deleted webhook
Set up a webhook endpoint in your Stripe Dashboard > Developers > Webhooks. Add an event listener for customer.subscription.deleted to capture cancellations in real time.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const bodyParser = require('body-parser');
const express = require('express');
const app = express();
app.post('/webhook', bodyParser.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
if (event.type === 'customer.subscription.deleted') {
const subscription = event.data.object;
console.log(`Subscription ${subscription.id} canceled`);
// Log to your database or analytics platform
}
res.json({received: true});
} catch (err) {
res.status(400).send(`Webhook Error: ${err.message}`);
}
});Store churn events and aggregate daily
For each webhook, record the customer ID, subscription ID, and cancellation timestamp. At the end of each day, aggregate to calculate rolling churn rates.
if (event.type === 'customer.subscription.deleted') {
const { customer, id, canceled_at } = event.data.object;
// Store in your database
await db.query(
'INSERT INTO churn_log (customer_id, subscription_id, canceled_at) VALUES ($1, $2, $3)',
[customer, id, new Date(canceled_at * 1000)]
);
}
// Daily aggregation
const today = new Date();
const startOfDay = new Date(today.setHours(0, 0, 0, 0));
const churnedToday = await db.query(
'SELECT COUNT(*) as count FROM churn_log WHERE canceled_at >= $1',
[startOfDay]
);
console.log(`Churn events today: ${churnedToday.rows[0].count}`);Common Pitfalls
- Confusing subscription
statuswith cancellation date. A subscription can becanceledbut still have an activecurrent_period_end—the customer hasn't lost access yet. - Forgetting to account for trial subscriptions. You may want to exclude trials from your churn calculation if they never paid.
- Using
createdtimestamp for cancellations instead ofcanceled_at. This shifts your numbers and makes trends hard to track. - Not handling webhook retries. Stripe retries failed webhooks, so you need idempotency to avoid double-counting cancellations.
Wrapping Up
Churn rate in Stripe boils down to pulling subscription data via the API, filtering for canceled subscriptions, and dividing by your baseline. Webhooks let you automate this and catch churn as it happens. If you want to track this automatically across tools, Product Analyst can help.