Skip to main content
RapidDev - Software Development Agency
API AutomationsStripeAPI Key

How to Automate Stripe Revenue Reports using the API

Automate Stripe revenue reports by calling POST /v1/reporting/report_runs to generate async financial summaries, then GET /v1/balance_transactions with date filters for real-time data. Stripe's Reporting API produces CSV files for activity summaries, payout reconciliation, and balance changes. Key limit: 100 req/sec in live mode, 25 req/sec in test mode. Always verify you're using live mode keys, not test keys.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read15-30 minutesStripeMay 2026RapidDev Engineering Team
TL;DR

Automate Stripe revenue reports by calling POST /v1/reporting/report_runs to generate async financial summaries, then GET /v1/balance_transactions with date filters for real-time data. Stripe's Reporting API produces CSV files for activity summaries, payout reconciliation, and balance changes. Key limit: 100 req/sec in live mode, 25 req/sec in test mode. Always verify you're using live mode keys, not test keys.

API Quick Reference

Auth

API Key (Bearer token)

Rate limit

100 requests/second (live mode)

Format

JSON

SDK

Available

Understanding the Stripe API

Stripe's REST API gives you complete programmatic access to your payment data. For revenue reporting, two key subsystems are relevant: the Balance Transactions API for real-time transaction-level data, and the Reporting API for pre-built financial reports that Stripe generates asynchronously as downloadable CSV files.

The Reporting API (available to all Stripe accounts) supports report types including balance activity summaries, payout reconciliation, and itemized balance changes. You request a report run via POST, poll for completion, then download the CSV. This is the cleanest path for automated bookkeeping pipelines.

For custom aggregations or dashboards, paginate GET /v1/balance_transactions with created[gte] and created[lte] filters. Each balance transaction record includes type (charge, refund, payout, fee), amount in smallest currency unit, net amount after fees, and the associated object ID. Official documentation: https://stripe.com/docs/api/reporting/report_run

Base URLhttps://api.stripe.com

Setting Up Stripe API Authentication

Stripe uses API key authentication — no OAuth flow required. You get a secret key (sk_*) and a publishable key (pk_*). For server-side reporting, you only need the secret key. The critical gotcha: test mode and live mode have separate keys and completely separate data sets. A common mistake is accidentally pulling test data thinking it's real revenue.

  1. 1Log in to your Stripe Dashboard at dashboard.stripe.com
  2. 2Click 'Developers' in the top navigation, then 'API keys'
  3. 3Copy your 'Secret key' — it starts with sk_live_ for production or sk_test_ for testing
  4. 4Store the key in an environment variable: export STRIPE_SECRET_KEY=sk_live_...
  5. 5Never commit the key to version control; add it to your .env file and .gitignore
  6. 6For restricted access, create a Restricted key under 'Developers > API keys > + Create restricted key' with only 'Read' access to Balance, Charges, and Reporting
auth.py
1import os
2import stripe
3
4stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')
5
6# Verify the key works
7try:
8 account = stripe.Account.retrieve()
9 print(f'Connected to account: {account.id} ({account.email})')
10except stripe.error.AuthenticationError as e:
11 print(f'Authentication failed: {e}')

Security notes

  • Store secret keys in environment variables, never hardcode them in source code
  • Use sk_test_* keys during development and sk_live_* only in production environments
  • Create Restricted API keys with read-only access to Balance and Reporting for reporting scripts
  • Never expose secret keys in frontend JavaScript, mobile apps, or public GitHub repositories
  • Rotate API keys immediately if you suspect exposure — go to Developers > API keys > Roll key
  • Use separate Stripe accounts or restricted keys for different team members to limit blast radius

Key endpoints

POST/v1/reporting/report_runs

Requests generation of a pre-built Stripe financial report. The report is generated asynchronously — you poll the returned ID for completion, then download the CSV file.

ParameterTypeRequiredDescription
report_typestringrequiredThe type of report to run. Common values: balance.summary.1 (balance activity summary), balance_transaction.summary.1 (transactions by type), payout_reconciliation.by_id.summary.1 (payout reconciliation)
parameters.interval_startnumberrequiredUnix timestamp for the start of the reporting period
parameters.interval_endnumberrequiredUnix timestamp for the end of the reporting period (must be in the past)
parameters.timezonestringoptionalIANA timezone name for report periods. Defaults to UTC.

Request

json
1{"report_type": "balance.summary.1", "parameters": {"interval_start": 1704067200, "interval_end": 1706745600, "timezone": "America/New_York"}}

Response

json
1{"id": "frr_1OqKLt2eZvKYlo2C7zXP8Bmv", "object": "reporting.report_run", "created": 1706745700, "livemode": true, "parameters": {"interval_start": 1704067200, "interval_end": 1706745600}, "report_type": "balance.summary.1", "status": "pending", "result": null}
GET/v1/reporting/report_runs/{id}

Polls the status of a report run. Once status is 'succeeded', the result.url field contains a time-limited download URL for the CSV file.

ParameterTypeRequiredDescription
idstringrequiredThe report run ID returned from the POST /v1/reporting/report_runs call

Response

json
1{"id": "frr_1OqKLt2eZvKYlo2C7zXP8Bmv", "object": "reporting.report_run", "status": "succeeded", "result": {"id": "file_1OqKMj2eZvKYlo2CqRdKjJem", "object": "file", "url": "https://files.stripe.com/v1/files/file_1OqKMj2eZvKYlo2CqRdKjJem/contents", "size": 4821, "created": 1706745820}}
GET/v1/balance_transactions

Returns a paginated list of all balance transactions. Use created[gte] and created[lte] with Unix timestamps to filter by date range. Each record shows the gross amount, fee, net amount, and transaction type.

ParameterTypeRequiredDescription
created[gte]numberoptionalUnix timestamp — return transactions created after this time
created[lte]numberoptionalUnix timestamp — return transactions created before this time
typestringoptionalFilter by transaction type: charge, refund, payout, fee, stripe_fee, etc.
limitnumberoptionalNumber of records per page, 1-100. Default 10.
starting_afterstringoptionalCursor for pagination — the last transaction ID from the previous page

Response

json
1{"object": "list", "data": [{"id": "txn_1OqKLt2eZvKYlo2C", "object": "balance_transaction", "amount": 2000, "available_on": 1706832000, "created": 1706745700, "currency": "usd", "description": "Payment for invoice INV-001", "fee": 88, "net": 1912, "status": "available", "type": "charge", "source": "ch_1OqKLt2eZvKYlo2Cabc"}], "has_more": true, "url": "/v1/balance_transactions"}
GET/v1/charges

Returns a paginated list of charges filtered by date. Useful for building custom revenue summaries with charge-level details including customer, payment method, and metadata.

ParameterTypeRequiredDescription
created[gte]numberoptionalUnix timestamp for start of date range filter
created[lte]numberoptionalUnix timestamp for end of date range filter
limitnumberoptionalRecords per page, max 100

Response

json
1{"object": "list", "data": [{"id": "ch_1OqKLt2eZvKYlo2C", "object": "charge", "amount": 2000, "amount_captured": 2000, "amount_refunded": 0, "currency": "usd", "customer": "cus_PqKLt2eZvKYlo2C", "description": "Subscription payment", "paid": true, "status": "succeeded", "created": 1706745700}], "has_more": true}

Step-by-step automation

1

Request a Financial Report Run

Why: The Reporting API generates structured CSV files that are easier to parse than raw transaction data, especially for bookkeeping and investor reports.

Send a POST to /v1/reporting/report_runs with the report type and date range as Unix timestamps. The report is generated asynchronously — Stripe returns a report run ID immediately with status 'pending'. Common report types: balance.summary.1 for a high-level view, balance_transaction.summary.1 for transaction breakdowns by type.

request.sh
1curl https://api.stripe.com/v1/reporting/report_runs \
2 -u "$STRIPE_SECRET_KEY:" \
3 -d report_type="balance.summary.1" \
4 -d "parameters[interval_start]"=1704067200 \
5 -d "parameters[interval_end]"=1706745600 \
6 -d "parameters[timezone]"="America/New_York"

Pro tip: Available report types are listed at GET /v1/reporting/report_types. Balance.summary.1 gives you gross revenue, fees, refunds, and net in one report. Use balance_transaction.summary.1 if you need transaction-level CSV rows.

Expected result: A JSON response with a report run ID (frr_*) and status 'pending'. Save this ID for polling.

2

Poll Until Report Generation Completes

Why: Stripe generates reports asynchronously — polling ensures you wait for the file before trying to download it.

Poll GET /v1/reporting/report_runs/{id} every 5-10 seconds until status changes from 'pending' to 'succeeded'. On success, the result.url field contains a time-limited download URL. If status is 'failed', inspect the error field and re-request with adjusted parameters.

request.sh
1# Replace frr_... with your actual report run ID
2curl https://api.stripe.com/v1/reporting/report_runs/frr_1OqKLt2eZvKYlo2C7zXP8Bmv \
3 -u "$STRIPE_SECRET_KEY:"

Pro tip: Use the REPORT_PROCESSING_FINISHED webhook event instead of polling. Subscribe to it and Stripe will POST your endpoint when the report is ready, eliminating polling entirely.

Expected result: The result.url field in the response becomes a download URL pointing to a Stripe-hosted CSV file.

3

Download and Parse the Report CSV

Why: The Stripe-hosted CSV file is time-limited and must be downloaded with your secret key for authentication.

The result.url is a Stripe Files endpoint URL. Download it using your secret key for HTTP Basic Auth (username = secret key, password empty). Parse the CSV to extract revenue totals, net amounts, and transaction counts. All amounts in the CSV are in the account's default currency, in major units (not cents).

request.sh
1# The result.url from the report run
2curl -u "$STRIPE_SECRET_KEY:" \
3 "https://files.stripe.com/v1/files/file_1OqKMj2eZvKYlo2CqRdKjJem/contents" \
4 -o revenue_report_jan2024.csv

Pro tip: For balance.summary.1, the CSV groups transactions by category (charge, refund, payout, stripe_fee). Sum the 'net' column for true net revenue after fees.

Expected result: A parsed array of rows from the revenue report CSV, with fields like gross_amount, fee, net, currency, and reporting_category.

4

Fetch Real-Time Transaction Data with Pagination

Why: For live dashboards or custom aggregations, the balance_transactions endpoint gives transaction-level data without waiting for async report generation.

Use GET /v1/balance_transactions with created[gte] and created[lte] date filters. The API paginates with cursor-based pagination — use the last ID in the response as starting_after for the next page. Amounts are in the smallest currency unit (cents for USD), so divide by 100 for display.

request.sh
1# Fetch balance transactions for January 2024
2curl "https://api.stripe.com/v1/balance_transactions?created[gte]=1704067200&created[lte]=1706745600&limit=100&type=charge" \
3 -u "$STRIPE_SECRET_KEY:"

Pro tip: Stripe's Python and Node SDKs support auto_paging_iter() / for await...of which handle pagination automatically. Use these instead of manual cursor loops.

Expected result: A full list of balance transactions for the date range with gross, fee, and net amounts ready for aggregation.

Complete working code

This script generates a Stripe balance summary report for the previous month, polls until it's ready, downloads the CSV, parses key revenue metrics, and sends a formatted summary to Slack. Run it daily or weekly via cron.

automate_stripe_revenue.py
1import stripe
2import requests
3import csv
4import io
5import os
6import time
7import logging
8from datetime import datetime, timezone, timedelta
9from calendar import monthrange
10
11logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
12log = logging.getLogger(__name__)
13
14stripe.api_key = os.environ['STRIPE_SECRET_KEY']
15SLACK_WEBHOOK = os.environ.get('SLACK_WEBHOOK_URL')
16
17def get_last_month_range():
18 today = datetime.now(timezone.utc)
19 first_of_month = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
20 last_month_end = first_of_month - timedelta(seconds=1)
21 last_month_start = last_month_end.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
22 return int(last_month_start.timestamp()), int(first_of_month.timestamp())
23
24def request_report(start_ts, end_ts):
25 run = stripe.reporting.ReportRun.create(
26 report_type='balance.summary.1',
27 parameters={'interval_start': start_ts, 'interval_end': end_ts}
28 )
29 log.info(f'Report run created: {run.id}')
30 return run.id
31
32def wait_for_report(run_id, max_wait=300):
33 deadline = time.time() + max_wait
34 while time.time() < deadline:
35 run = stripe.reporting.ReportRun.retrieve(run_id)
36 if run.status == 'succeeded':
37 return run.result.url
38 elif run.status == 'failed':
39 raise Exception(f'Report failed: {run.error}')
40 log.info(f'Report status: {run.status}, waiting...')
41 time.sleep(10)
42 raise TimeoutError('Report timed out')
43
44def download_and_parse(url):
45 resp = requests.get(url, auth=(stripe.api_key, ''))
46 resp.raise_for_status()
47 reader = csv.DictReader(io.StringIO(resp.text))
48 return list(reader)
49
50def send_slack(message):
51 if SLACK_WEBHOOK:
52 requests.post(SLACK_WEBHOOK, json={'text': message})
53 log.info('Slack notification sent')
54
55def main():
56 start_ts, end_ts = get_last_month_range()
57 period = datetime.fromtimestamp(start_ts, timezone.utc).strftime('%B %Y')
58 log.info(f'Generating revenue report for {period}')
59
60 run_id = request_report(start_ts, end_ts)
61 download_url = wait_for_report(run_id)
62 rows = download_and_parse(download_url)
63
64 gross = net = fees = 0
65 for row in rows:
66 try:
67 gross += float(row.get('gross', 0) or 0)
68 net += float(row.get('net', 0) or 0)
69 fees += float(row.get('fee', 0) or 0)
70 except (ValueError, KeyError):
71 pass
72
73 currency = rows[0].get('currency', 'usd').upper() if rows else 'USD'
74 message = (
75 f'*Stripe Revenue Report — {period}*\n'
76 f'Gross Revenue: {currency} {gross:,.2f}\n'
77 f'Stripe Fees: {currency} {fees:,.2f}\n'
78 f'Net Revenue: {currency} {net:,.2f}'
79 )
80 log.info(message)
81 send_slack(message)
82
83if __name__ == '__main__':
84 main()

Error handling

401No such API key: 'sk_test_...'
Cause

Invalid, revoked, or mismatched API key. Most commonly: using a test key in production or a live key in a test environment.

Fix

Verify the key in your Stripe Dashboard under Developers > API keys. Check that STRIPE_SECRET_KEY environment variable is set correctly. Use sk_test_* for testing, sk_live_* for production.

Retry strategy

No retry — fix the key first

429Too Many Requests
Cause

Exceeded Stripe's rate limit of 100 req/sec in live mode or 25 req/sec in test mode. Usually happens during bulk transaction pagination.

Fix

Slow down your request loop. Use the Stripe SDK's auto-pagination which handles rate limits automatically. Add time.sleep(0.1) between manual requests.

Retry strategy

Honor the Retry-After header if present. Otherwise exponential backoff: 1s, 2s, 4s, 8s. Stripe typically recovers within seconds.

400interval_end must be before the current time
Cause

Report interval_end timestamp is in the future. Reporting can only be run on past time periods.

Fix

Ensure interval_end is at least a few minutes in the past. For daily reports, set interval_end to the start of today (midnight UTC) to report on the previous day.

Retry strategy

No retry — fix the timestamp parameters

400Report type not available for your account
Cause

Some report types require specific account configurations or are only available for accounts in certain countries.

Fix

Check the available report types at GET /v1/reporting/report_types. Use balance.summary.1 which is universally available, or fall back to GET /v1/balance_transactions for custom aggregations.

Retry strategy

No retry — use an alternate report type

500An error occurred with our connection to Stripe
Cause

Transient Stripe server error. Rare but possible during high load periods.

Fix

Retry with exponential backoff. Stripe's status page at status.stripe.com shows active incidents.

Retry strategy

Exponential backoff: retry after 1s, 2s, 4s, 8s, 16s — max 5 retries

Rate Limits for Stripe API

ScopeLimitWindow
Live mode100 requestsper second
Test mode25 requestsper second
Report runsAsync processing timeTypically 10-60 seconds per report
retry-handler.ts
1import time
2
3def stripe_request_with_retry(fn, max_retries=5):
4 for attempt in range(max_retries):
5 try:
6 return fn()
7 except stripe.error.RateLimitError:
8 wait = 2 ** attempt
9 print(f'Rate limited, waiting {wait}s...')
10 time.sleep(wait)
11 except stripe.error.APIConnectionError:
12 if attempt == max_retries - 1:
13 raise
14 time.sleep(2 ** attempt)
15 raise Exception('Max retries exceeded')
  • Use Stripe SDK auto-pagination (auto_paging_iter in Python, for await...of in Node) which handles rate limits automatically
  • Request reports asynchronously via the Reporting API instead of paginating millions of balance transactions
  • For bulk historical fetches, use the Reporting API's balance_transaction.itemized.1 report type which returns all transactions as a single CSV
  • Cache report results — don't re-request the same month's report daily; re-generate only when the period changes
  • Subscribe to the report.succeeded webhook to avoid polling

Security checklist

  • Store STRIPE_SECRET_KEY in environment variables, never in source code or git repositories
  • Use sk_live_* keys only in production — use sk_test_* for all development and CI/CD
  • Create Restricted API keys with read-only access to Balance and Reporting for this specific automation
  • Never log the full API key — log only the last 4 characters for identification (sk_live_...abcd)
  • Rotate API keys every 90 days or immediately after any suspected exposure
  • If accepting webhooks, verify the Stripe-Signature header using stripe.webhooks.constructEvent() with the raw body
  • Use HTTPS for all API calls — the Stripe library enforces this by default
  • Restrict server access — revenue report scripts should only run from your backend, never from client-side code

Automation use cases

Daily Revenue Slack Alert

beginner

Run every morning and post yesterday's gross revenue, fees, and net to a Slack channel for the founding team.

Monthly Investor Report Email

intermediate

Generate a formatted monthly P&L from Stripe data and email it to your cap table automatically on the 1st of each month.

Google Sheets Revenue Dashboard

intermediate

Pull daily balance transactions and append them to a Google Sheet, building a running revenue dashboard with charts.

Bookkeeping Sync to QuickBooks

advanced

Download the payout reconciliation report and sync transactions to QuickBooks or Xero via their APIs for automated accounting.

No-code alternatives

Don't want to write code? These platforms can automate the same workflows visually.

Zapier

Free tier (100 tasks/month), paid from $19.99/month

Zapier's Stripe integration triggers on payment events (new charge, new customer) and can send data to Google Sheets, Slack, or email — but it works event-by-event, not as a batch report.

Pros
  • + No code required
  • + 200+ destination apps
  • + Real-time event triggers
Cons
  • - No access to Reporting API or balance transactions
  • - Event-based only, not period-based reporting
  • - Costs per task at scale

Make

Free tier (1,000 ops/month), paid from $9/month

Make's Stripe module supports batch operations and can paginate balance transactions on a schedule, sending formatted summaries to Slack, email, or Google Sheets.

Pros
  • + Visual workflow builder
  • + Better batch support than Zapier
  • + Google Sheets integration works well
Cons
  • - Limited to REST API calls, no Reporting API access
  • - More setup than Zapier
  • - Monthly operation limits

n8n

Free (self-hosted), Cloud from €20/month

n8n's HTTP Request node can call the Stripe Reporting API directly, with a Code node to parse CSV results and a Slack node to send the formatted report.

Pros
  • + Full API access including Reporting endpoints
  • + Self-hostable for data privacy
  • + Free on self-hosted
Cons
  • - Requires technical setup
  • - CSV parsing needs custom code
  • - No Stripe-specific module (uses HTTP Request)

Best practices

  • Always divide amounts by 100 when displaying — Stripe returns amounts in smallest currency units (cents for USD)
  • Use the Reporting API for periodic reports and balance_transactions for real-time dashboards — they serve different use cases
  • Implement idempotency by storing the report run ID — if the script crashes after requesting a report, retrieve the existing run instead of creating a new one
  • Filter balance transactions by type (charge, refund, payout) separately to build a proper P&L breakdown
  • Test your automation in test mode using sk_test_* keys before running against live data
  • Store downloaded report CSVs in S3 or Google Cloud Storage for audit trails
  • Set up the REPORT_PROCESSING_FINISHED webhook to get notified when large reports complete instead of polling

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm automating Stripe revenue reporting using the Stripe API in Python. I use stripe.reporting.ReportRun.create() to request a balance.summary.1 report, then poll for status='succeeded', then download the CSV with Basic auth. The CSV rows have fields: reporting_category, currency, gross, fee, net. Help me: (1) parse the CSV to calculate total net revenue, (2) handle the case where the report fails with status='failed', and (3) add exponential backoff retry logic for 429 and 500 errors.

Lovable / V0 Prompt

Build a Stripe revenue dashboard in React with these features: (1) date range picker for start/end dates, (2) button to trigger a Stripe revenue report via a backend API endpoint, (3) polling status indicator while the report generates, (4) results displayed as a summary card showing Gross Revenue, Stripe Fees, and Net Revenue, (5) a line chart showing daily net revenue over the selected period using Recharts. The backend API endpoint POST /api/stripe/report accepts {start_date, end_date} and returns {gross, fees, net, currency, period}.

Frequently asked questions

Is the Stripe Reporting API free?

Yes. The Reporting API is available to all Stripe accounts at no additional charge. You only pay Stripe's standard processing fees (2.9% + $0.30 per transaction). The API itself, including report runs and balance transaction queries, has no separate cost.

What's the difference between balance_transactions and the Reporting API?

GET /v1/balance_transactions returns individual transaction records in real-time and is ideal for custom aggregations and live dashboards. The Reporting API generates pre-built financial summaries (activity, payouts, balance changes) as CSV files asynchronously — better for periodic reports, bookkeeping exports, and large date ranges. For most revenue reporting use cases, the Reporting API is cleaner.

What happens when I hit the rate limit?

Stripe returns a 429 Too Many Requests error. The response may include a Retry-After header indicating how long to wait. In the Stripe SDK, RateLimitError is thrown. Implement exponential backoff: wait 1s after the first 429, then 2s, 4s, 8s. For reporting workloads, you rarely hit the 100 req/sec live mode limit — it's more likely in test mode (25 req/sec).

Why is my revenue report showing test mode data instead of real payments?

This is the most common Stripe reporting mistake. Test mode and live mode are completely isolated — they have separate API keys, separate customers, and separate transactions. Verify you're using sk_live_* (not sk_test_*) for production data. Check the livemode field in any Stripe response: true means live data, false means test data.

How far back can I pull historical revenue data?

The Stripe API has no enforced historical limit — you can query balance transactions from the day your account was created. However, very large date ranges (years of data) should use the Reporting API's itemized report types rather than paginating balance_transactions directly, since the latter can take many minutes with thousands of pages.

Can I automate sending revenue reports to email or Google Sheets?

Yes. After downloading the CSV via the Reporting API, parse it and send via SendGrid or Resend (email) or append rows via the Google Sheets API. For Slack, POST to an incoming webhook URL with a formatted message. The complete Python script in this guide includes Slack integration as an example.

Can RapidDev help build a custom Stripe revenue dashboard?

Yes. RapidDev has built 600+ apps including billing dashboards, investor reporting tools, and multi-payment-processor analytics platforms. If you need a custom Stripe reporting solution beyond what the standard API offers, get a free consultation at rapidevelopers.com.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.