How to calculate MRR from Stripe

Stellan Bergström ·

Stripe's dashboard shows you an MRR number. The Stripe API does not. There is no GET /v1/mrr, no field on the customer, nothing on the balance - if you want the figure in your own tool, a leaderboard, or a spreadsheet, you have to rebuild the arithmetic from the subscriptions endpoint yourself. This is how to do it so your number matches the one Stripe shows, and the traps that make most home-grown MRR scripts wrong.

What "MRR" means to Stripe

Stripe's dashboard MRR is the sum of the monthly-normalised value of every active subscription, net of discounts. Concretely:

  • Only subscriptions with status = "active" count. Trials (trialing), past_due, unpaid, incomplete, paused and canceled are excluded.
  • Each subscription item's price is normalised to one month using its billing interval.
  • Coupons attached to the subscription are subtracted.
  • Taxes, one-off invoice items, and usage-based (metered) charges are not included.

If you follow exactly those rules you'll reproduce the dashboard to the cent. If you deviate on any of them - most commonly by counting trials - you'll be reporting a number nobody else can verify. (For the general definition see what MRR is.)

Step 1: get a key that can only read

Use a restricted key (rk_live_…) with read access to Subscriptions, not your secret key. A restricted key can't create charges, refund, or touch your balance, so if it leaks the damage is "someone can see your subscription list", not "someone can move your money". Create one under Developers → API keys → Create restricted key, and grant Read on Subscriptions (the expand below also needs Read on Coupons, which is bundled into Subscriptions access in current Stripe).

Note the _live_ vs _test_ in the key: test-mode subscriptions are fake by definition and must never be counted as revenue.

Step 2: list active subscriptions and expand the coupons

GET /v1/subscriptions?status=active&limit=100
    &expand[]=data.discounts.source.coupon

Paginate with starting_after until has_more is false. Each subscription carries items.data[], and each item has a price with unit_amount, currency, and a recurring object holding interval (day, week, month, year) and interval_count.

The expand is important. Discounts are returned as references by default; expanding them gives you the coupon's percent_off or amount_off so you can net it out in the same pass.

Step 3: normalise every item to a month

For each subscription item:

monthsPerPeriod = interval_count × monthsPerUnit[interval]
monthly = (unit_amount × quantity) ÷ monthsPerPeriod

with the standard business conventions (52 weeks and 365 days to a year):

intervalmonths per unit
day12 ÷ 365
week12 ÷ 52
month1
year12

So a $790/year plan is 790 ÷ 12 = $65.83 per month; a $20/week plan is 20 ÷ (12 ÷ 52) = $86.67; a $300 plan billed every 3 months (interval: month, interval_count: 3) is $100. Keep everything in cents as integers until the very end - floating-point drift across a few hundred subscriptions is enough to disagree with the dashboard.

Sum the items to get the subscription's gross monthly value.

Step 4: subtract discounts

For each valid coupon on the subscription:

  • percent_off → multiply the gross monthly value by (1 − percent_off ÷ 100).
  • amount_off → this is a per-invoice reduction, so normalise it by the subscription's billing period the same way you normalised the price: amount_off ÷ monthsPerPeriod, then subtract.

A subscription can't go negative; clamp at zero. Coupons with valid: false (expired or exhausted) are ignored.

Step 5: add it up, and mind the currency

MRR = the sum of every subscription's net monthly value. If you bill in more than one currency, sum per currency first and convert to one reporting currency with a single consistent rate. Mixing them silently is one of the most common ways an MRR figure drifts from reality - an exchange-rate move shows up as "growth".

What to leave out (and why people include it)

  • Trials. trialing subscriptions haven't paid. Stripe's own MRR excludes them; so should yours, even with a card on file.
  • Past-due. The last invoice failed. It might recover; until it does it's not revenue.
  • Metered / usage-based items. Their unit_amount is per unit of usage, not per period, so normalising them produces nonsense. Stripe excludes them from MRR; report usage revenue separately.
  • One-off invoice items and setup fees. Not recurring.
  • Scheduled cancellations. A subscription with cancel_at_period_end: true is still active and still counts until it actually ends. That's correct - the customer is still paying - but flag it as churn-in-waiting in your own reporting.

A minimal implementation sketch

const MONTHS = { day: 12 / 365, week: 12 / 52, month: 1, year: 12 };

let totalCents = 0;
for await (const sub of stripe.subscriptions.list({
  status: "active",
  expand: ["data.discounts.source.coupon"],
})) {
  let gross = 0;
  let repPeriod = 1;
  for (const item of sub.items.data) {
    const r = item.price.recurring;
    if (!r || item.price.unit_amount == null) continue; // metered / custom
    const months = r.interval_count * MONTHS[r.interval];
    repPeriod = months;
    gross += (item.price.unit_amount * (item.quantity ?? 1)) / months;
  }
  let net = gross;
  for (const d of sub.discounts ?? []) {
    const c = typeof d === "string" ? null : d.source?.coupon;
    if (!c || typeof c === "string" || !c.valid) continue;
    if (c.percent_off) net *= 1 - c.percent_off / 100;
    if (c.amount_off) net -= c.amount_off / repPeriod;
  }
  totalCents += Math.max(0, net);
}
console.log((totalCents / 100).toFixed(2), "MRR");

Twenty lines, and it agrees with the dashboard. The rest of the work is the boring part: pagination, rate limits, retries, and storing a snapshot so you can show growth over time.

How HitMRR does it

HitMRR runs exactly this computation every 30 minutes for every connected startup: restricted read-only key, status=active only, interval normalisation with the ×52/12 convention, coupons netted out, integer cents throughout. It then converts each startup's figure to USD with a daily exchange rate so founders billing in EUR, GBP or SEK rank fairly against USD startups on the leaderboard. Keys are AES-256-GCM encrypted at rest and the interface never shows more than their last four characters; a key containing _test_ is accepted for testing but silently hidden from every public board.

That is what "verified MRR" means in practice: not a screenshot, but the same arithmetic Stripe uses, run by a third party that can't edit the result.

Frequently asked questions

Does Stripe have an MRR API?

No. The dashboard shows MRR under Billing, but there is no API endpoint or field that returns it. You compute it from the Subscriptions API: active subscriptions, prices normalised to monthly, discounts subtracted.

Which subscription statuses count toward MRR?

Only 'active'. Trialing, past_due, unpaid, incomplete, paused and canceled subscriptions are excluded, matching Stripe's own dashboard figure.

How do I convert a yearly Stripe price to monthly?

Divide by 12 - more generally, divide the unit amount by interval_count times the months in one interval (year = 12, month = 1, week = 12/52, day = 12/365).

Do coupons reduce MRR?

Yes. A percent_off coupon scales the subscription's monthly value; an amount_off coupon is a per-invoice reduction, so divide it by the billing period in months before subtracting.

What key should I use to read subscriptions?

A restricted key (rk_live_...) with read access to Subscriptions. Never share a secret key (sk_live_...); it can move money. HitMRR rejects secret keys outright.

KEEP READING