Monthly cohort retention, from a sample export
Each row is a group of customers by the month of their first paid charge. Each cell is the share of that group who paid again that many months later. Built from a synthetic Stripe payments export (252 rows, 72 customers, the real export's 23 column names).
| First paid month | Customers | Month 0 | Month 1 | Month 2 | Month 3 | Month 4 | Month 5 | Month 6 | Month 7 | Month 8 |
|---|---|---|---|---|---|---|---|---|---|---|
| 2026-01 | 12 | 100% | 75% | 58% | 42% | 33% | 25% | 25% | 25% | 17% |
| 2026-02 | 12 | 100% | 75% | 50% | 42% | 33% | 33% | 8% | 17% | |
| 2026-03 | 12 | 100% | 50% | 33% | 33% | 25% | 17% | 17% | ||
| 2026-04 | 12 | 100% | 83% | 75% | 58% | 42% | 42% | |||
| 2026-05 | 12 | 100% | 75% | 50% | 33% | 33% | ||||
| 2026-06 | 12 | 100% | 58% | 50% | 42% |
Drop your export here
Use the payments CSV from your Stripe Dashboard. xmetric finds the Created date (UTC), Customer ID and Status columns, keeps rows whose status is Paid, and rebuilds this table from your rows in this browser. Nothing is uploaded.
No file loaded yet. You can also drop the CSV on this box.
Export the payments CSV from Stripe
- In the Stripe Dashboard, open the Transactions (Payments) page.
- Click Export.
- Pick the date range and time zone. The default columns include everything this page needs.
- Download the CSV and drop it in the box above.
Steps from Stripe's support page, checked 2026-10-17. If you chose custom columns, pick the date, customer and status columns in the box.
The calculation in Tableau
Three calculated fields. Put Cohort on Rows, Months since first payment on Columns and Retention on Text, with Retention computed along Months since first payment.
// Cohort
DATETRUNC('month', {FIXED [Customer ID] : MIN(IF [Status] = "Paid" THEN [Created date (UTC)] END)})
// Months since first payment
DATEDIFF('month',
{FIXED [Customer ID] : MIN(IF [Status] = "Paid" THEN [Created date (UTC)] END)},
[Created date (UTC)])
// Retention (table calculation)
COUNTD(IF [Status] = "Paid" THEN [Customer ID] END)
/ LOOKUP(COUNTD(IF [Status] = "Paid" THEN [Customer ID] END), FIRST())Why the IF sits inside the FIXED: a normal filter on Status runs after FIXED, so a failed first charge would still set the cohort month. Putting the condition inside, or making Status a context filter, keeps failed charges out. See the filter order on your own data.
The same table in SQL
Runs in DuckDB on the raw export. Every number in the sample table above comes from this query.
WITH paid AS (
SELECT "Customer ID" AS customer_id,
CAST("Created date (UTC)" AS TIMESTAMP) AS created
FROM read_csv_auto('unified_payments.csv')
WHERE "Status" = 'Paid' AND "Customer ID" IS NOT NULL
), firsts AS (
SELECT customer_id, date_trunc('month', MIN(created)) AS cohort
FROM paid GROUP BY customer_id
), activity AS (
SELECT DISTINCT p.customer_id, f.cohort,
date_diff('month', f.cohort, date_trunc('month', p.created)) AS month_number
FROM paid p JOIN firsts f USING (customer_id)
)
SELECT cohort, month_number, COUNT(*) AS customers,
COUNT(*) / FIRST_VALUE(COUNT(*)) OVER (PARTITION BY cohort ORDER BY month_number) AS retention
FROM activity
GROUP BY cohort, month_number
ORDER BY cohort, month_number;Ask an agent to do it
Paste this into Claude, ChatGPT or Codex with your export attached. It works with any agent that can run code.
I attached a Stripe payments export (unified_payments.csv). Keep rows where Status is "Paid" and Customer ID is not empty. For each Customer ID, the cohort is the month of their first paid charge. For each cohort and each month since (0, 1, 2, ...), count distinct customers with a paid charge in that month, and divide by the cohort's month-0 count. Return a table: one row per cohort, one column per month, as percentages. Show the SQL you ran.
The xmetric MCP server, in development, will build this view for an agent directly.
Reading the table
Month 0 is always 100%: it's the month each customer first paid. Read across a row to see one cohort fade; read down a column to compare cohorts at the same age. A later cohort holding a higher month-3 number than an earlier one means retention improved. Empty cells are months that haven't happened yet for that cohort.
Stripe is a trademark of Stripe, Inc. xmetric is not affiliated with Stripe. The sample file is synthetic: made-up customers using the column names of Stripe's default payments export as of 2026-10-17.
Questions people ask
Why count customers instead of revenue?
Customer retention shows who keeps paying. Revenue retention also counts upgrades and downgrades; it needs the Amount column and a sum instead of a distinct count.
Do refunds or failed charges count?
Only rows whose status is Paid count. A failed charge that was retried and paid counts once, in the month it was paid.
Is my Stripe data uploaded?
No. The CSV is read by code in this browser tab. Nothing is sent anywhere.
Drop your Stripe export above, or download the sample to see the format.