Calculate MRR from the Stripe API

Updated 2026-08-04

Stripe has no MRR endpoint. You build it from active subscriptions.

The approach

  1. List subscriptions with status=active
  2. Expand price data so you get amount and interval in one call
  3. Normalise each line to a monthly figure
  4. Sum

The code

type Interval = "day" | "week" | "month" | "year";

const MONTHS_PER: Record<Interval, number> = {
  day: 12 / 365,
  week: 12 / 52,
  month: 1,
  year: 12,
};

/** Monthly contribution of a recurring charge. */
function toMonthly(amount: number, interval: Interval, count = 1): number {
  const cycles = MONTHS_PER[interval] * Math.max(1, count);
  return cycles > 0 ? amount / cycles : 0;
}

/** Stripe reports minor units. Not every currency has two decimals. */
function fromMinor(amount: number, currency = "usd"): number {
  const zeroDecimal = new Set(["jpy", "krw", "vnd", "clp", "isk"]);
  return zeroDecimal.has(currency.toLowerCase()) ? amount : amount / 100;
}

async function trueMrr(key: string): Promise<number> {
  let mrr = 0;
  let startingAfter: string | undefined;

  for (;;) {
    const qs = new URLSearchParams({
      status: "active",
      limit: "100",
      "expand[]": "data.items.data.price",
    });
    if (startingAfter) qs.set("starting_after", startingAfter);

    const res = await fetch(`https://api.stripe.com/v1/subscriptions?${qs}`, {
      headers: { Authorization: `Bearer ${key}` },
    });
    if (!res.ok) throw new Error(`stripe ${res.status}`);
    const page = await res.json();

    for (const sub of page.data) {
      for (const item of sub.items.data) {
        const price = item.price;
        if (!price?.recurring || price.unit_amount == null) continue;
        const major = fromMinor(price.unit_amount, price.currency);
        mrr += toMonthly(major, price.recurring.interval,
                         price.recurring.interval_count || 1)
               * (item.quantity ?? 1);
      }
    }

    if (!page.has_more) break;
    startingAfter = page.data[page.data.length - 1].id;
  }

  return mrr;
}

The four traps

Pagination. Stripe returns 10 by default, 100 maximum. Miss the has_more loop and everything past your first page silently vanishes. This is the most common bug in homegrown MRR scripts, and it fails quietly — your number is just wrong.

Minor units. Stripe reports cents, so you divide by 100 — except for zero-decimal currencies like JPY and KRW, where you must not. Get this wrong and your yen revenue is 100× too small.

Quantity. A subscription item can have quantity > 1 for seat-based pricing. Ignoring it undercounts every multi-seat customer.

Multiple items. One subscription can carry several items — a base plan plus add-ons. Reading only items.data[0] misses the rest.

Status filtering

status=active excludes trialling, past-due and cancelled subscriptions, which is what you want. See what counts as an active subscription for the reasoning, particularly around past-due.

Permissions

A restricted key with read on Subscriptions, Prices and Products is enough for this. Nothing needs write access. Full list: Stripe key permissions for reporting.

If you'd rather not maintain this

The code above is roughly what FRGMNT's Stripe adapter does — and then the same toMonthly runs against Lemon Squeezy, Paddle and the rest, because the normalisation is platform-independent. See normalising subscription intervals in code, or query it from the terminal.

Frequently asked

Which Stripe API endpoint gives you MRR?

None directly. You list active subscriptions with their price data expanded, normalise each item to a monthly amount, and sum. Stripe has no single MRR endpoint.

Do I need to paginate the subscriptions endpoint?

Yes. Stripe returns 10 items by default and 100 at most. Above 100 active subscriptions you must follow the has_more and starting_after cursor or your MRR will be silently truncated.

Read next