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,pausedandcanceledare 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):
interval | months per unit |
|---|---|
day | 12 ÷ 365 |
week | 12 ÷ 52 |
month | 1 |
year | 12 |
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.
trialingsubscriptions 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_amountis 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: trueis stillactiveand 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.