Generating Churn Rate Reports from Stripe Excel Exports in Python for Zapier and n8n
This article shows how to convert raw Excel invoice exports from Stripe into a repeatable, investor-ready churn rate report you can call from Zapier, Make, or n8n. You will get a concrete Python implementation that reads a Stripe-style Excel export, computes monthly customer churn and MRR churn for a specified window, and writes CSV/JSON outputs suitable for automated investor updates.
We target indie hackers and small agencies who manage multiple clients and need a reliable way to pipeline Stripe invoice exports into a signed-off metric for investor updates or weekly decks.
What this function expects and produces
Input: an Excel file (XLSX) exported from Stripe invoices. The script expects a sheet containing one row per invoice with these columns (exact names used in code below):
- invoice_id (string)
- customer_id (string)
- status (string) — e.g., paid, open, void
- amount_due_cents (integer) — total in cents
- currency (string) — e.g., USD
- invoice_date (ISO 8601 string or Excel datetime) — e.g., 2024-03-15T10:00:00Z
- plan_interval (string) — monthly or yearly
Processing steps (concrete): parse timestamps, filter to paid invoices, normalize amounts to monthly MRR-equivalent (divide yearly amounts by 12), compute per-customer last-paid dates and per-month active sets, compute monthly customer churn rate and MRR churn, and export a CSV and JSON summary.
Output: CSV file (e.g., churn_report_2024-03.csv) with columns month (YYYY-MM), active_customers, churned_customers, customer_churn_rate, starting_mrr_usd, churned_mrr_usd, mrr_churn_rate; plus a JSON summary for APIs.
Real-world scenario
Imagine an agency with 3 clients. For a single client you have a single Excel export named client_acme_invoices.xlsx containing 1,200 invoice rows for 2023-2024 and roughly 450 unique customer_ids. Investors want a monthly churn table covering the last 6 months, plus an explanation of how churn was measured.
Concrete example input row (CSV-like):
invoice_id,customer_id,status,amount_due_cents,currency,invoice_date,plan_interval
in_001,cust_12,paid,2000,USD,2024-02-05T15:12:00Z,monthly
in_002,cust_12,paid,24000,USD,2024-01-01T09:00:00Z,yearly
in_003,cust_24,void,0,USD,2024-01-12T08:00:00Z,monthly
Example dataset and the exact problem
Dataset: 1,000 rows of Stripe invoice exports covering 12 months with columns above. About 300 unique customers, mix of monthly and yearly plans. Problem to solve: investors want a deterministic monthly customer churn rate and corresponding MRR churn for the last 6 months. The challenge: Stripe exports include yearly invoices (need conversion), refunds/void rows, and inconsistent date formats.
Step-by-step workflow (end-to-end)
- Get the monthly Stripe invoice export XLSX for a client (file: client_acme_invoices.xlsx).
- Upload the file to an automation platform (Zapier/Make) or place in a cloud bucket.
- Call the churn-report Python API (or run locally) to compute a 6-month churn table.
- Save outputs: churn_report_2024-03.csv and churn_summary_2024-03.json.
- Attach CSV/JSON to investor update email or push into Notion/Google Sheets via Zapier.
Algorithm (how churn is calculated)
- Filter invoices to status == 'paid' and normalize invoice_date to UTC.
- Convert amount_due_cents to monthly MRR-equivalent: if plan_interval == 'yearly', amount / 12; else amount as-is.
- For each calendar month M compute: active_start = customers with a paid invoice in (M-1 window, i.e., within 30 days before M start).
- churned_in_M = subset of active_start who have no paid invoice in the 30 days after M start.
- customer_churn_rate = churned_in_M / len(active_start); mrr_churn = sum(MRR of churned customers) / starting_mrr.
Python implementation example
The snippet below is a minimal, runnable example using pandas and openpyxl. It reads an XLSX, computes a 6-month churn table, and writes CSV/JSON outputs.
import pandas as pd
from datetime import timedelta
def generate_churn_report(xlsx_path, months=6, churn_window_days=30, out_prefix='churn_report'):
df = pd.read_excel(xlsx_path, engine='openpyxl')
# normalize column names expected by this script
df = df.rename(columns=str.strip)
df['invoice_date'] = pd.to_datetime(df['invoice_date'], utc=True)
# keep only paid invoices
df = df[df['status'].str.lower() == 'paid'].copy()
# convert cents to USD float
df['amount_usd'] = df['amount_due_cents'] / 100.0
# normalize to monthly MRR-equivalent
df['mrr_usd'] = df.apply(lambda r: r['amount_usd'] / 12.0 if r['plan_interval']=='yearly' else r['amount_usd'], axis=1)
last_date = df['invoice_date'].max().normalize()
start_month = (last_date - pd.offsets.MonthBegin(months-1)).normalize()
rows = []
for i in range(months):
month_start = (start_month + pd.DateOffset(months=i)).normalize()
month_label = month_start.strftime('%Y-%m')
before_window_start = month_start - pd.Timedelta(days=churn_window_days)
after_window_end = month_start + pd.Timedelta(days=churn_window_days)
# active at start: paid in the churn_window_days before month_start
active_start = set(df[(df['invoice_date'] >= before_window_start) & (df['invoice_date'] < month_start)]['customer_id'].unique())
# customers with paid invoice in the churn window after month_start
active_after = set(df[(df['invoice_date'] >= month_start) & (df['invoice_date'] < after_window_end)]['customer_id'].unique())
churned = sorted(list(active_start - active_after))
starting_mrr = df[df['customer_id'].isin(active_start)].groupby('customer_id')['mrr_usd'].max().sum()
churned_mrr = df[df['customer_id'].isin(churned)].groupby('customer_id')['mrr_usd'].max().sum()
customer_churn_rate = (len(churned) / len(active_start)) if active_start else 0.0
mrr_churn_rate = (churned_mrr / starting_mrr) if starting_mrr else 0.0
rows.append({
'month': month_label,
'active_customers': len(active_start),
'churned_customers': len(churned),
'customer_churn_rate': round(customer_churn_rate, 4),
'starting_mrr_usd': round(starting_mrr, 2),
'churned_mrr_usd': round(churned_mrr, 2),
'mrr_churn_rate': round(mrr_churn_rate, 4)
})
out_df = pd.DataFrame(rows)
csv_path = f'{out_prefix}.csv'
json_path = f'{out_prefix}.json'
out_df.to_csv(csv_path, index=False)
out_df.to_json(json_path, orient='records', date_format='iso')
return {'csv': csv_path, 'json': json_path}
# Example call
if __name__ == '__main__':
result = generate_churn_report('client_acme_invoices.xlsx', months=6)
print('Wrote:', result)
How Functory Makes It Easy
To publish this as a Functory function, wrap the core logic in a single Python main(...) entrypoint that accepts typed parameters: main(file_path: str, months: int = 6, churn_window_days: int = 30) and returns a JSON-like dict or path to the generated CSV. On Functory you must pick an exact Python version (for example, 3.11.11) and provide a requirements.txt with pinned versions, one per line (e.g., pandas==2.1.1, openpyxl==3.1.2). The platform will expose the parameters as inputs in the web UI and as JSON fields on the HTTP API.
Implementation notes for Functory:
- Put your code in a file with main(...) that reads the uploaded file path (FilePath) and writes a path-like return (e.g., '/tmp/churn_report.csv').
- List dependencies pinned to exact versions so Functory provisions the correct environment.
- Triggering: you can call the Functory HTTP API from Zapier/Make/n8n by uploading the XLSX (file multipart) or providing a cloud URL; Functory returns the CSV file URL once processing finishes.
Benefits: no servers to manage, automatic CPU scaling, built-in logging (print()), and pay-per-use billing. You can chain functions — e.g., pre-processing in one function, churn calculation in another, and a reporting function to push results into Notion or Google Sheets.
Alternatives and why this approach is better
Common alternatives: manual spreadsheets where analysts filter and pivot; using the Stripe Dashboard (manual snapshots); or custom ETL pipelines in Airflow/Dagster. Manual spreadsheets are error-prone and slow; Stripe Dashboard gives visualizations but not reproducible CSVs for investor decks; heavy ETL systems are overkill for indie hackers.
This function-based approach is superior because it is reproducible, automatable (callable via Zapier/n8n), lightweight (single-file script to maintain), and gives consistent definitions (exact churn_window_days and conversion rules). For small teams, it replaces a manual 2–3 hour monthly task with a single API call.
Business impact
Quantified benefit: converting a manual spreadsheet process to this automated API reduces per-report preparation time from ~2 hours to under 5 minutes — a ~95% reduction in manual time. For an agency creating 10 client updates per month, that’s ~20 hours saved monthly.
Industry context
According to a 2024 OpenView SaaS benchmark report, median annual gross revenue churn for SaaS companies ranges around 5–7% depending on stage and product (OpenView, 2024). Investors commonly request month-over-month churn tables to validate these numbers.
Comparison to other developer workflows
Notebooks: quick to prototype but hard to automate and integrate with Zapier/n8n. CLI scripts: reproducible but require servers or cron jobs to be kept running. Functory-style API: one-file logic that becomes a hosted endpoint, removing deployment friction while keeping reproducibility and auditability.
Conclusion: Converting Stripe Excel exports into deterministic churn reports enables fast, reproducible investor updates. Next steps: try the sample script on one client's XLSX, tune churn_window_days for your billing cadence, and publish the code as a Functory function for automated Zapier integration. Once automated, you can instrument further — e.g., attach a PDF of the chart to investor emails or push the CSV into a BI dataset for trend analysis.
Thanks for reading.
