6 min read

How to Track ARR in Stripe

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.

javascript
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)
);
Fetch active subscriptions and filter to only recurring items

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.

javascript
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,
    });
  });
});
Watch out: Some subscriptions have mixed items—one monthly, one one-time. Always filter by 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.

javascript
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 };
}
Calculate total ARR and break it down by customer

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.

javascript
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)}`);
Tip: Gross ARR (before discounts) is clean for trend analysis. If you use coupons, compare against invoice data to get net ARR for cash forecasting.

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.

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

javascript
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]);
  }
}
Watch out: Webhooks can arrive out of order or be retried. Use the subscription's 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.recurring to 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_end dates 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.

Track these metrics automatically

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

Try Product Analyst — Free