ARR tells you how much revenue you're generating annually from subscriptions, but Stripe doesn't calculate it for you automatically. You need to extract subscription data and aggregate it yourself. Here's how to build reliable ARR monitoring in Stripe without external tools.
Calculate ARR from Active Subscriptions
The foundation of ARR monitoring is querying your active subscriptions and summing their annual value. Stripe's API lets you paginate through subscriptions and filter by status.
List all active subscriptions and sum annual value
Use the subscriptions.list() method with status: 'active' filter. For each subscription, iterate through its line items and multiply the price by the annual frequency. Handle both monthly and annual billing intervals by normalizing to months first.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
async function calculateARR() {
let arr = 0;
let hasMore = true;
let startingAfter = undefined;
while (hasMore) {
const subscriptions = await stripe.subscriptions.list({
status: 'active',
limit: 100,
starting_after: startingAfter,
});
subscriptions.data.forEach((sub) => {
sub.items.data.forEach((item) => {
if (item.price.recurring) {
const monthlyAmount = item.price.recurring.interval === 'month'
? item.price.unit_amount
: item.price.unit_amount / 12;
arr += monthlyAmount * 12 / 100; // Convert from cents to dollars
}
});
});
hasMore = subscriptions.has_more;
startingAfter = subscriptions.data[subscriptions.data.length - 1]?.id;
}
return arr;
}
const currentARR = await calculateARR();
console.log(`Current ARR: $${currentARR.toFixed(2)}`);
Track MRR as a foundation for ARR
Monthly Recurring Revenue (MRR) is easier to validate and multiply by 12 to approximate ARR. Query active subscriptions, sum their monthly amounts, and store this daily. The difference month-over-month tells you growth or churn instantly.
async function calculateMRR() {
let mrr = 0;
const subscriptions = await stripe.subscriptions.list({
status: 'active',
limit: 100,
});
subscriptions.data.forEach((sub) => {
sub.items.data.forEach((item) => {
if (item.price.recurring) {
const monthlyAmount = item.price.recurring.interval === 'month'
? item.price.unit_amount
: item.price.unit_amount / 12;
mrr += monthlyAmount / 100;
}
});
});
return mrr;
}
const currentMRR = await calculateMRR();
console.log(`Current MRR: $${currentMRR.toFixed(2)}`);
console.log(`Estimated ARR: $${(currentMRR * 12).toFixed(2)}`);
paused and past_due subscriptions unless you're tracking at-risk revenue separately.Monitor ARR Changes with Webhooks
Real-time ARR updates beat nightly batch jobs. Stripe sends webhooks whenever subscriptions change—new signups, plan upgrades, and cancellations. Listen for these events and update your ARR tracking instantly.
Set up webhook handlers for subscription lifecycle events
Configure a webhook endpoint to receive customer.subscription.created, customer.subscription.updated, and customer.subscription.deleted events. Use webhooks.constructEvent() to verify the signature and parse the event.
const express = require('express');
const app = express();
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'customer.subscription.created') {
const subscription = event.data.object;
let newARRValue = 0;
subscription.items.data.forEach((item) => {
if (item.price.recurring) {
const monthlyAmount = item.price.recurring.interval === 'month'
? item.price.unit_amount
: item.price.unit_amount / 12;
newARRValue += monthlyAmount * 12 / 100;
}
});
console.log(`Subscription created: +$${newARRValue.toFixed(2)} ARR`);
// Increment your ARR total here
}
if (event.type === 'customer.subscription.deleted') {
const subscription = event.data.object;
let canceledARRValue = 0;
subscription.items.data.forEach((item) => {
if (item.price.recurring) {
const monthlyAmount = item.price.recurring.interval === 'month'
? item.price.unit_amount
: item.price.unit_amount / 12;
canceledARRValue += monthlyAmount * 12 / 100;
}
});
console.log(`Subscription canceled: -$${canceledARRValue.toFixed(2)} ARR`);
// Decrement your ARR total here
}
res.status(200).json({received: true});
});
app.listen(3000, () => console.log('Webhook endpoint listening'));
Capture plan changes and upgrades via subscription.updated
When a customer upgrades or downgrades their plan, Stripe fires a customer.subscription.updated event. Check the previous_attributes field to see what changed and calculate the ARR delta.
if (event.type === 'customer.subscription.updated') {
const subscription = event.data.object;
const previous = event.data.previous_attributes;
// Check if items (plan/price) changed
if (previous.items) {
const oldARR = calculateSubscriptionARR(previous.items.data);
const newARR = calculateSubscriptionARR(subscription.items.data);
const delta = newARR - oldARR;
console.log(`Subscription updated: ${delta > 0 ? '+' : ''}$${delta.toFixed(2)} ARR`);
// Update your ARR total by the delta
}
}
function calculateSubscriptionARR(items) {
let arr = 0;
items.forEach((item) => {
if (item.price.recurring) {
const monthlyAmount = item.price.recurring.interval === 'month'
? item.price.unit_amount
: item.price.unit_amount / 12;
arr += monthlyAmount * 12 / 100;
}
});
return arr;
}
stripe listen --forward-to localhost:3000/webhook with the Stripe CLI to test locally.Store and Trend ARR Over Time
Point-in-time ARR is useful, but trends matter more. Store daily snapshots of your ARR so you can track month-over-month growth and detect churn early.
Schedule daily ARR snapshots in your database
Run a scheduled job (cron job, Lambda, or similar) once per day to calculate total ARR and insert it into your database with a timestamp. This creates a historical record you can query for trend analysis.
const cron = require('node-cron');
const db = require('./db'); // Your database client
// Run daily at 2 AM UTC
cron.schedule('0 2 * * *', async () => {
try {
const arr = await calculateARR();
const today = new Date().toISOString().split('T')[0];
await db.query(
'INSERT INTO arr_snapshots (date, arr, created_at) VALUES ($1, $2, $3)',
[today, arr, new Date()]
);
console.log(`ARR snapshot stored: $${arr.toFixed(2)} on ${today}`);
} catch (err) {
console.error('Failed to store ARR snapshot:', err);
}
});
Calculate growth metrics from historical snapshots
Query your snapshots table to compute month-over-month growth rate, churn rate, and net ARR expansion. These metrics drive business decisions and help you spot trends early.
async function calculateGrowthMetrics() {
const today = new Date().toISOString().split('T')[0];
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
.toISOString().split('T')[0];
const currentSnapshot = await db.query(
'SELECT arr FROM arr_snapshots WHERE date = $1',
[today]
);
const previousSnapshot = await db.query(
'SELECT arr FROM arr_snapshots WHERE date = $1',
[thirtyDaysAgo]
);
if (currentSnapshot.rows.length && previousSnapshot.rows.length) {
const current = currentSnapshot.rows[0].arr;
const previous = previousSnapshot.rows[0].arr;
const momGrowth = ((current - previous) / previous) * 100;
console.log(`Current ARR: $${current.toFixed(2)}`);
console.log(`ARR 30 days ago: $${previous.toFixed(2)}`);
console.log(`MoM Growth: ${momGrowth.toFixed(2)}%`);
}
}
await calculateGrowthMetrics();
Common Pitfalls
- Forgetting to normalize billing intervals—annual subscriptions are worth 1/12th per month, always convert to a common period before summing
- Including paused and past_due subscriptions in ARR—track these separately as at-risk revenue, not core ARR
- Working with amounts as integers instead of dividing by 100—Stripe stores amounts in cents, always convert to dollars before reporting
- Running ARR calculations synchronously during request handling—offload to a background job to avoid API timeouts
Wrapping Up
ARR monitoring in Stripe requires three steps: query active subscriptions and sum their annual value, listen to webhooks for real-time updates, and store daily snapshots for trend analysis. This gives you reliable revenue visibility without external tools. If you want to track this automatically across tools, Product Analyst can help.