// guides
How to track MRR for an iOS app
App Store Connect will tell you yesterday's sales, but not your monthly recurring revenue. Here's how to compute a defensible MRR from Apple's data (annual-plan normalization, trials, grace periods, proceeds vs price, currency) and the architecture to keep it honest.
last updated 2026-07-11 · by revenuehog
Why App Store Connect has no MRR number
App Store Connect reports what Apple needs to pay you: units, sales, proceeds, by day and territory. MRR is a derived, opinionated metric. It requires deciding that a $39.99/yr subscriber is "worth" $3.33/mo, that a free trial is worth $0, and that someone whose card failed yesterday still counts. Apple doesn't make those calls for you, so neither App Store Connect nor its App Analytics shows a normalized MRR series. Every iOS MRR number you've ever seen was computed by somebody, from the data below.
Defining MRR for App Store subscriptions
The definition that survives contact with Apple's data has three rules:
- ▸Normalize every billing period to months.Each subscriber contributes their per-period price divided by the period's length in months. Annual → ÷12, monthly → ÷1, weekly → ÷~0.23.
- ▸Decide who counts as paying. Active subscriptions, paid intro offers and promotional offers count. Grace-period and billing-retry subscribers should too: they still have entitlement and usually recover; excluding them makes MRR oscillate with card failures. Free trials never count: they pay $0. Track them as their own metric.
- ▸Intro offers at the price actually paid. A $0.99-first-month intro contributes $0.99, not the eventual $4.99. MRR states the present, not the forecast.
// The core of every App Store MRR number: normalize each active
// subscription's price to a monthly figure, then sum.
type ActiveSubRow = {
pricePerPeriod: number; // what the subscriber pays per billing period
periodMonths: number; // 1 monthly, 12 annual, ~0.23 weekly (7 / 30.44)
quantity: number; // subscribers on this exact (product, price, state)
};
function monthlyRecurring(row: ActiveSubRow): number {
if (row.periodMonths <= 0) return 0; // parse bug guard, not a real plan
return (row.pricePerPeriod / row.periodMonths) * row.quantity;
}
const mrr = rows
.filter((r) => isPayingState(r)) // active, paid intro, grace, billing retry
.reduce((sum, r) => sum + monthlyRecurring(r), 0);
// $39.99/yr → 39.99 / 12 ≈ $3.33/mo per subscriber
// $1.99/wk → 1.99 / 0.23 ≈ $8.65/mo per subscriberThe two data sources, and what each can't tell you
| Daily SUBSCRIPTION report | Server Notifications V2 | |
|---|---|---|
| What it is | A once-daily snapshot of active subscriptions per product, price, state and country (App Store Connect API) | A signed HTTPS POST per lifecycle event (subscribe, renew, cancel, expire, refund) as it happens |
| Freshness | Lags about a day | Real time (once wired) |
| History | About a year of daily reports back (Apple's limit) | From the moment you set the URL (plus a 180-day replay API) |
| Best for | MRR and subscriber levels: it's the authoritative count | The live feed, per-customer history, and signals reports omit (like auto-renew being turned off) |
| Blind spots | No auto-renew status: a cancelled-but-unexpired trial looks identical to a live one. No per-transaction identity. | No pre-wiring history; events, not levels (you'd have to replay state to derive MRR from events alone) |
The pragmatic architecture uses both: levels from the report, moments from the notifications.Compute MRR from the daily snapshot; use the event stream for the feed and for corrections the report can't see. Trying to derive MRR purely from events means re-implementing Apple's subscription state machine. Doable, and a bug farm.
Levels vs flows (the classic dashboard bug)
The SUBSCRIPTION report doesn't arrive every day, and "no report" does not mean "zero subscribers". Subscriber counts and MRR are levels: on a gap day, carry the last observation forward. New subscriptions and revenue are flows: a missing day really is zero. Get this backwards and your MRR chart shows heart-attack cliffs on Apple's report-processing hiccups.
// Apple's SUBSCRIPTION report is a daily *snapshot* of levels, and some
// days simply have no report. A missing day doesn't mean zero subscribers.
// Carry the last observed value forward (LOCF) when building the series.
function fillLocf(
axis: string[], // every day on the chart, "YYYY-MM-DD"
byDay: Map<string, number>, // days that actually reported
): { date: string; value: number }[] {
let last = 0;
return axis.map((date) => {
if (byDay.has(date)) last = byDay.get(date)!;
return { date, value: last };
});
}
// The opposite rule applies to flows (new subscriptions/day, revenue/day):
// a missing day there really is zero. Confusing levels with flows is
// the classic MRR-dashboard bug.Two related habits keep the numbers defensible: anchor "current MRR" to the most recent day that actually has data (and label it "as of" that day, since reports lag), and compute period-over-period deltas against the gap-filled value 30 days before the anchor, with no delta shown when the prior value is zero, because "+∞%" helps nobody.
Proceeds vs customer price
Apple takes 30%, or 15% if you're enrolled in the App Store Small Business Program. That gives every subscription two MRR figures: gross (customer price) and net(your proceeds). One note from production: Apple's daily reports already state Developer Proceeds at your actual rate — 85% if you're in the Small Business Program — so report-derived net MRR needs no adjustment. The only time the share matters is when you derive proceeds from a customer price yourself (a live per-event estimate), where you apply your own rate. The proceeds-math guide covers the distinction (and its caveats) in full.
Currency
Apple reports revenue per territory in dozens of local currencies. Convert each day's rows at that day's rate (the ECB publishes free daily reference rates) rather than one frozen rate, and flag anything you couldn't convert with a published rate as an estimate. Whatever you build, treat Apple's payout reports as the source of truth for exact amounts; a telemetry MRR that admits it's ±ECB-rate accuracy is honest, one that claims payout precision is lying.
The DIY architecture, sketched
- ▸Ingest: an App Store Connect API key (.p8, Issuer ID, Key ID) pulling the daily SALES and SUBSCRIPTION reports (gzipped TSV) into per-day tables, backfilling the ~365 days Apple allows.
- ▸Live events: an HTTPS endpoint receiving Server Notifications V2, verifying each JWS against Apple's root, deduping on
notificationUUID. - ▸Compute: the normalization above, run over the latest snapshot day; LOCF-filled series for charts; flows summed per window.
- ▸Convert: daily FX rates keyed by (date, currency), applied at read or ingest time.
- ▸Reconcile:monthly, against Apple's payout reports. Drift here is how you find bugs in all of the above.
It's a real but bounded project: a few weeks to solid, most of it in the edge cases this guide flagged. Paying states, gap days, Small Business proceeds, currency.
Or skip the build
RevenueHog computes exactly this: the same normalization, paying states, LOCF series, ECB conversion and Small Business adjustment, documented formula-by-formula on the metrics methodology page (no black boxes: you can check our math against this guide).
FAQ
Does App Store Connect show MRR?
Do free trials count toward MRR?
How do annual subscriptions convert to MRR?
Should MRR use customer price or my proceeds?
Is MRR the same as monthly revenue?
sources
Apple: App Store Connect API, Sales and Trends reports (SALES/SUBSCRIPTION report types, daily granularity limits) — developer.apple.com/documentation/appstoreconnectapi.
Apple: App Store Small Business Program (15% commission) — developer.apple.com/app-store/small-business-program.
European Central Bank: euro foreign exchange reference rates — ecb.europa.eu.
Checked 2026-07-11.
// related