# Net Revenue Retention: The One Metric Your AI Can't Source

> How to calculate net revenue retention from Stripe, why no single endpoint returns it, and how to get a weekly churn read where every figure is sourced.

- Published: 2026-07-29 · Updated: 2026-07-29 · jurniti
- Canonical: https://www.jurniti.com/blog/net-revenue-retention-stripe-template

Here is the formula everybody ranks for:

**NRR = (Starting MRR + Expansion − Contraction − Churn) ÷ Starting MRR**

Take the cohort of customers you had on day one of the period. Add what they upgraded into. Subtract what they downgraded out of and what they cancelled. Divide by where they started.

Above 100% means that group of customers is worth more today than it was at the start — without a single new signup.

That is the definition. Now the part the glossary pages skip.

## The formula is four inputs, and Stripe hands you one

Starting MRR is the easy one. Fetch active subscriptions, normalise every price to a monthly amount, sum it. Annual plans divide by twelve, quarterly by three, weekly multiply by about 4.345.

The other three are not lookups. They are reconstructions.

**Expansion and contraction** are per-subscription *deltas*. You need to know that customer 4,110 went from two seats to five on the 14th, and that customer 812 dropped a $99 add-on on the 22nd. Stripe knows both. It just does not have an endpoint called `/expansion`.

**Churn** is the one that quietly breaks the most spreadsheets. The obvious move is to list subscriptions with `status=canceled` and filter by date. That returns the wrong week, every week — because `created` on a cancelled subscription is when it *started*, not when it ended. A subscription that began in March and died last Tuesday will not appear in your "last 7 days" filter at all.

The correct source is the event log: `customer.subscription.deleted` events inside your window. Different endpoint, different mental model, and the reason a lot of hand-rolled churn queries report a suspiciously stable zero.

## The gap where a confident number appears

Now put yourself in the position of an AI assistant asked "what was our NRR last month?"

It can reach Stripe. It can pull subscriptions and invoices. It can see that the formula requires expansion and contraction across a fixed cohort. What it cannot do is find those two numbers sitting in a field somewhere.

So it does what a helpful assistant does. It produces a figure.

Not maliciously — the number will be *plausible*. It will be in the range you'd expect. It will sit in the report next to four figures that are completely real, which is exactly what makes it dangerous. You will not catch it by reading, because it does not look wrong. It looks like the others.

Then somebody puts it in a board deck.

## What a defensible number actually requires

Two things, and only the second one is unusual.

The first is that a machine does the arithmetic. Not the model — actual code, in a real language, reading real rows. Arithmetic performed inside prose is where a grounded report quietly stops being grounded, because nobody can re-run a sentence.

The second is a checker that reads the finished report and blocks it.

That is the design of the [Stripe AI Churn Analysis & Revenue Report](/templates/stripe-ai-churn-analysis-revenue-report) template on jurniti. Five read-only Stripe calls come back. Python computes every rate, total, and delta, and stamps each one with the call it came from and the method used. The agent reads that bundle and writes the prose — it never divides.

Then a grounding check pulls every figure out of the draft and compares it against the bundle. A number that is not in there does not get posted:

```
UNGROUNDED: these figures are not in the facts bundle:
  line 12: 118%  → post blocked
```

The posting script runs that check itself rather than trusting whoever called it, so there is no version of the workflow where you skip the gate by forgetting.

Here is the honest part. **The template does not compute NRR.** Cohort-level expansion and contraction tracking is not in the kit today. So when you ask it for net revenue retention, it does not approximate — it reports the metric as unavailable and says why.

What it does compute, each one sourced: MRR, active subscribers, churned MRR and count, notice-given MRR and count, past-due MRR and count, collected, refunded, invoices paid, invoices retrying, gross MRR churn, logo churn, and ARPU.

A tool that tells you which number it cannot give you is worth more than one that gives you all of them.

## Churn rate formula, without the benchmark theatre

Two churn rates matter and they answer different questions.

**Gross MRR churn** = churned MRR ÷ (active MRR + churned MRR), for the period. This is money. It ignores logo count entirely, which is correct — losing one $4,000 account is not the same event as losing one $29 account, even though both are "a customer."

**Logo churn** = cancelled subscriptions ÷ (active + cancelled). This is count. Run both, because the gap between them is the diagnosis: high logo churn with low MRR churn means your small accounts are leaving, which is a pricing or onboarding story, not a retention crisis.

As for *what is a good churn rate* — the honest answer is that the benchmark posts are close to useless. Self-serve SMB products routinely sit at 3-5% monthly logo churn and survive. Annual enterprise contracts would call 1% monthly a fire. Your comparison set is your own trend line from eight weeks ago, not a median someone published about a different business.

And note what NRR alone hides. NRR can print 115% while you are bleeding logos, because expansion from the accounts that stayed papers over the ones that left. That is why gross revenue retention exists — it counts only the losses and is capped at 100%. Report both or report neither.

## Involuntary churn is a different problem wearing the same costume

Some of your churn is not a decision. It is an expired card.

Failed payments surface in Stripe as `past_due` and `unpaid` subscriptions with invoices still retrying. Lumping them into your churn number is a category error, because the fix is completely different: a customer who chose to leave needs a conversation, and a customer whose Visa expired needs an email with a link.

The template splits them. Its at-risk list is built from a rule rather than a judgement call — a subscription is at risk when it has given notice (`cancel_at_period_end`) or when payment is failing (`past_due`). Then each of those customers is matched to your PostHog data by email to answer one question: are they still using the product?

That is the tiebreaker. A past-due account logging in daily needs an invoice fixed. A past-due account nobody has seen in three weeks needs a phone call today. Same Stripe status, opposite response, and you cannot tell them apart from billing data alone.

## What you actually get

Fork it, paste a **restricted, read-only** Stripe key, and type `/pulse`.

The read lands in Slack: MRR, what churned, who gave notice, whose card is failing, and what to do first. Every figure carries its source. Anything retrieval could not reach is listed under "Not available" instead of quietly becoming a zero.

The key has to be restricted, by the way. Hand it a full `sk_` secret key and it refuses before making a single call — a reporting agent has no business holding credentials that can write to your billing system.

Before you trust it with anything, run `bin/selftest.py`. Fourteen checks, offline, no credentials. It tries to slip a fabricated figure past the gate and confirms the gate stops it, then hands the collector a writable key and confirms it refuses.

That is the whole pitch. Not that the numbers are right — that you can check them.

No free trial and no free tier. Forking provisions a real paid microVM, and the first purchase carries a 30-day money-back guarantee. Your Stripe key, your Slack workspace, your model spend, all on a box that is yours.

## Frequently asked questions

### What is net revenue retention?

Net revenue retention (NRR) measures how much recurring revenue you keep from an existing cohort of customers over a period, including expansion, after subtracting contraction and churn. Starting MRR plus expansion minus contraction minus churn, divided by starting MRR. Above 100% means the cohort grew without a single new customer.

### How do you calculate net revenue retention from Stripe?

You cannot get it from one endpoint. You need a fixed cohort of subscriptions as of the period start, then each of those subscriptions' expansion, contraction, and cancellation during the period. Stripe returns the pieces across subscriptions, subscription items, invoices, and the event log, but assembling them into a cohort is your job, not an API call.

### What is the difference between gross and net revenue retention?

Gross revenue retention (GRR) counts only the losses: contraction and churn, floored at 100%. Net revenue retention adds expansion back in, so NRR can exceed 100% while GRR never can. Reporting NRR without GRR hides whether growth is coming from keeping customers or from upselling the ones who stayed.

### What is a good churn rate?

It depends entirely on segment and price point, which is why benchmark posts are close to useless without context. Self-serve SMB products routinely run 3-5% monthly logo churn; annual enterprise contracts measure churn yearly and treat 1% monthly as bad. Compare yourself to your own trend, not to a blog post's median.

### What is involuntary churn?

Revenue you lose to failed payments rather than to a customer decision: expired cards, insufficient funds, issuer declines. It shows up in Stripe as past_due and unpaid subscriptions with retrying invoices. It is worth separating because the fix is a dunning email, not a save call.

### Does the jurniti template calculate NRR?

No, and it says so rather than approximating. NRR needs cohort-level expansion and contraction tracking the kit does not do today. It computes MRR, churned MRR, notice-given MRR, past-due MRR, collected, refunded, gross MRR churn, logo churn, and ARPU, each carrying the exact Stripe call it came from. Anything it cannot retrieve is reported as missing.

### Is there a free trial?

No free trial and no free tier. Forking provisions a real paid microVM. The first purchase carries a 30-day money-back guarantee. Your Stripe key, your Slack, your model spend.
