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
API Key (Bearer token)
100 requests/second (live mode)
JSON
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
https://api.stripe.comSetting 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.
- 1Log in to your Stripe Dashboard at dashboard.stripe.com
- 2Click 'Developers' in the top navigation, then 'API keys'
- 3Copy your 'Secret key' — it starts with sk_live_ for production or sk_test_ for testing
- 4Store the key in an environment variable: export STRIPE_SECRET_KEY=sk_live_...
- 5Never commit the key to version control; add it to your .env file and .gitignore
- 6For restricted access, create a Restricted key under 'Developers > API keys > + Create restricted key' with only 'Read' access to Balance, Charges, and Reporting
1import os2import stripe34stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')56# Verify the key works7try: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
/v1/reporting/report_runsRequests 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
report_type | string | required | The 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_start | number | required | Unix timestamp for the start of the reporting period |
parameters.interval_end | number | required | Unix timestamp for the end of the reporting period (must be in the past) |
parameters.timezone | string | optional | IANA timezone name for report periods. Defaults to UTC. |
Request
1{"report_type": "balance.summary.1", "parameters": {"interval_start": 1704067200, "interval_end": 1706745600, "timezone": "America/New_York"}}Response
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}/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.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | The report run ID returned from the POST /v1/reporting/report_runs call |
Response
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}}/v1/balance_transactionsReturns 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
created[gte] | number | optional | Unix timestamp — return transactions created after this time |
created[lte] | number | optional | Unix timestamp — return transactions created before this time |
type | string | optional | Filter by transaction type: charge, refund, payout, fee, stripe_fee, etc. |
limit | number | optional | Number of records per page, 1-100. Default 10. |
starting_after | string | optional | Cursor for pagination — the last transaction ID from the previous page |
Response
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"}/v1/chargesReturns a paginated list of charges filtered by date. Useful for building custom revenue summaries with charge-level details including customer, payment method, and metadata.
| Parameter | Type | Required | Description |
|---|---|---|---|
created[gte] | number | optional | Unix timestamp for start of date range filter |
created[lte] | number | optional | Unix timestamp for end of date range filter |
limit | number | optional | Records per page, max 100 |
Response
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
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.
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.
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.
1# Replace frr_... with your actual report run ID2curl 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.
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).
1# The result.url from the report run2curl -u "$STRIPE_SECRET_KEY:" \3 "https://files.stripe.com/v1/files/file_1OqKMj2eZvKYlo2CqRdKjJem/contents" \4 -o revenue_report_jan2024.csvPro 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.
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.
1# Fetch balance transactions for January 20242curl "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.
1import stripe2import requests3import csv4import io5import os6import time7import logging8from datetime import datetime, timezone, timedelta9from calendar import monthrange1011logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')12log = logging.getLogger(__name__)1314stripe.api_key = os.environ['STRIPE_SECRET_KEY']15SLACK_WEBHOOK = os.environ.get('SLACK_WEBHOOK_URL')1617def 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())2324def 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.id3132def wait_for_report(run_id, max_wait=300):33 deadline = time.time() + max_wait34 while time.time() < deadline:35 run = stripe.reporting.ReportRun.retrieve(run_id)36 if run.status == 'succeeded':37 return run.result.url38 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')4344def 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)4950def send_slack(message):51 if SLACK_WEBHOOK:52 requests.post(SLACK_WEBHOOK, json={'text': message})53 log.info('Slack notification sent')5455def 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}')5960 run_id = request_report(start_ts, end_ts)61 download_url = wait_for_report(run_id)62 rows = download_and_parse(download_url)6364 gross = net = fees = 065 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 pass7273 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)8283if __name__ == '__main__':84 main()Error handling
No such API key: 'sk_test_...'Invalid, revoked, or mismatched API key. Most commonly: using a test key in production or a live key in a test environment.
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.
No retry — fix the key first
Too Many RequestsExceeded Stripe's rate limit of 100 req/sec in live mode or 25 req/sec in test mode. Usually happens during bulk transaction pagination.
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.
Honor the Retry-After header if present. Otherwise exponential backoff: 1s, 2s, 4s, 8s. Stripe typically recovers within seconds.
interval_end must be before the current timeReport interval_end timestamp is in the future. Reporting can only be run on past time periods.
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.
No retry — fix the timestamp parameters
Report type not available for your accountSome report types require specific account configurations or are only available for accounts in certain countries.
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.
No retry — use an alternate report type
An error occurred with our connection to StripeTransient Stripe server error. Rare but possible during high load periods.
Retry with exponential backoff. Stripe's status page at status.stripe.com shows active incidents.
Exponential backoff: retry after 1s, 2s, 4s, 8s, 16s — max 5 retries
Rate Limits for Stripe API
| Scope | Limit | Window |
|---|---|---|
| Live mode | 100 requests | per second |
| Test mode | 25 requests | per second |
| Report runs | Async processing time | Typically 10-60 seconds per report |
1import time23def 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 ** attempt9 print(f'Rate limited, waiting {wait}s...')10 time.sleep(wait)11 except stripe.error.APIConnectionError:12 if attempt == max_retries - 1:13 raise14 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
beginnerRun every morning and post yesterday's gross revenue, fees, and net to a Slack channel for the founding team.
Monthly Investor Report Email
intermediateGenerate 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
intermediatePull daily balance transactions and append them to a Google Sheet, building a running revenue dashboard with charts.
Bookkeeping Sync to QuickBooks
advancedDownload 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/monthZapier'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.
- + No code required
- + 200+ destination apps
- + Real-time event triggers
- - 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/monthMake's Stripe module supports batch operations and can paginate balance transactions on a schedule, sending formatted summaries to Slack, email, or Google Sheets.
- + Visual workflow builder
- + Better batch support than Zapier
- + Google Sheets integration works well
- - Limited to REST API calls, no Reporting API access
- - More setup than Zapier
- - Monthly operation limits
n8n
Free (self-hosted), Cloud from €20/monthn8n'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.
- + Full API access including Reporting endpoints
- + Self-hostable for data privacy
- + Free on self-hosted
- - 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.
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.
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.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation