Stripe doesn't have an out-of-the-box ARR metric, but your subscription data is all there—you just need to query it right. If you're running a SaaS business, ARR is critical to understand, and calculating it from Stripe means you're working with live data rather than spreadsheet guesses.
Fetch active subscriptions and identify recurring revenue
ARR in Stripe starts with subscriptions. You need to pull your active recurring subscriptions and understand their pricing intervals.
Retrieve all active subscriptions with recurring prices
Use the Stripe SDK to list subscriptions with status active. Filter to include only subscriptions with recurring prices—not one-time charges. The Subscriptions API returns each subscription with its pricing details, including the interval field that tells you how often the customer is charged.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const subscriptions = await stripe.subscriptions.list({
status: 'active',
expand: ['data.items.price'],
limit: 100,
});
const recurringSubscriptions = subscriptions.data.filter(sub =>
sub.items.data.some(item => item.price.recurring)
);Inspect the pricing interval
Each subscription's items contain prices with a recurring object. Inside that object is interval (day, week, month, or year) and interval_count (how many intervals between charges). For ARR calculation, monthly subscriptions count as 12× their monthly amount; annual subscriptions use the amount as-is.
recurringSubscriptions.forEach(sub => {
sub.items.data.forEach(item => {
const recurring = item.price.recurring;
console.log({
customer: sub.customer,
interval: recurring.interval,
intervalCount: recurring.interval_count,
amount: item.price.unit_amount,
});
});
});price.recurring to exclude one-time charges from ARR.Calculate ARR from subscription amounts
With subscriptions in hand, convert each to an annual figure. Monthly subscriptions multiply by 12; annual subscriptions use the amount directly.
Build an ARR calculation function
Iterate through each subscription and its items. Calculate the annual revenue by checking the interval. For monthly subscriptions, multiply by 12. For annual subscriptions, use the amount as-is. Account for interval_count if a subscription bills every 2 months or similar. Store the result keyed by customer so you can track which accounts contribute what ARR.
function calculateARR(subscriptions) {
let totalARR = 0;
const arrByCustomer = {};
subscriptions.forEach(sub => {
let customerARR = 0;
sub.items.data.forEach(item => {
const amount = item.price.unit_amount; // in cents
const recurring = item.price.recurring;
let annualAmount = 0;
if (recurring.interval === 'month') {
annualAmount = (amount * 12) / (recurring.interval_count || 1);
} else if (recurring.interval === 'year') {
annualAmount = amount / (recurring.interval_count || 1);
}
customerARR += annualAmount;
});
arrByCustomer[sub.customer] = customerARR / 100;
totalARR += customerARR;
});
return { totalARR: totalARR / 100, byCustomer: arrByCustomer };
}Exclude trials and handle discounts
If a customer is on a trial, exclude them from ARR calculations until the trial ends. Check trial_end against the current timestamp. For discounts, Stripe applies them at invoice time, not at the price object level. Raw subscription amounts give you gross ARR—if you need net ARR after coupons, you'll need to check recent invoices instead.
const activeRecurringSubscriptions = subscriptions.data.filter(sub => {
const isActive = sub.status === 'active';
const notOnTrial = !sub.trial_end || sub.trial_end < Math.floor(Date.now() / 1000);
const hasRecurring = sub.items.data.some(item => item.price.recurring);
return isActive && notOnTrial && hasRecurring;
});
const arrData = calculateARR(activeRecurringSubscriptions);
console.log(`Total ARR: $${arrData.totalARR.toFixed(2)}`);Track ARR changes with webhooks
Subscriptions change constantly—upgrades, downgrades, cancellations. Use webhooks to update your ARR in real-time rather than recalculating everything hourly.
Set up subscription webhooks in Stripe Dashboard
Go to Developers > Webhooks > Add endpoint and listen for customer.subscription.created, customer.subscription.updated, and customer.subscription.deleted. Stripe retries failed webhooks for 3 days, so make sure your handler is idempotent—if the same webhook fires twice, your ARR tracking shouldn't double-count.
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.raw({type: 'application/json'}), (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.startsWith('customer.subscription')) {
const subscription = event.data.object;
handleSubscriptionChange(subscription);
}
res.json({received: true});
});Handle subscription events and update ARR tracking
When a webhook fires, fetch the updated subscription and recalculate its ARR contribution. If a subscription is active, update your database. If canceled, remove it from your ARR total. This keeps your dashboard current without re-querying all subscriptions every hour.
async function handleSubscriptionChange(subscription) {
const fullSub = await stripe.subscriptions.retrieve(subscription.id, {
expand: ['items.data.price'],
});
if (fullSub.status === 'active') {
let arr = 0;
fullSub.items.data.forEach(item => {
const amount = item.price.unit_amount;
const recurring = item.price.recurring;
if (recurring.interval === 'month') {
arr += (amount * 12) / (recurring.interval_count || 1);
} else if (recurring.interval === 'year') {
arr += amount / (recurring.interval_count || 1);
}
});
await db.query(
'UPDATE arr_tracking SET amount = $1, updated = NOW() WHERE customer_id = $2',
[arr / 100, fullSub.customer]
);
} else if (fullSub.status === 'canceled') {
await db.query('DELETE FROM arr_tracking WHERE customer_id = $1', [fullSub.customer]);
}
}updated timestamp to ignore stale events and prevent race conditions.Common Pitfalls
- Forgetting that Stripe doesn't count one-time charges as recurring revenue—filter by
price.recurringto avoid inflating ARR with add-ons. - Mixing gross ARR (before discounts) with net ARR (after coupons)—decide which you're tracking and verify against actual invoices.
- Not handling subscription downgrades correctly—when a customer switches from annual to monthly, your ARR calculation must reflect the new interval immediately.
- Treating trials as active revenue—always exclude customers with active
trial_enddates from ARR calculations.
Wrapping Up
Tracking ARR in Stripe requires pulling subscription data, filtering for recurring revenue, and calculating annualized amounts. The core logic is simple—multiply monthly amounts by 12, use annual amounts as-is—but the real value comes from automating updates via webhooks so your numbers stay current as subscriptions change. If you want to track this automatically across tools, Product Analyst can help.