Revenue reporting in Stripe is straightforward in concept but scattered in practice. Stripe tracks every transaction, fee, and refund—but whether you're looking at gross revenue, net settlement, or something in between depends on where you're checking. This matters for financial accuracy, tax compliance, and forecasting.
What Stripe Actually Reports
Stripe tracks several revenue dimensions. Knowing which one answers your question saves time.
Charges: The Core Revenue Unit
A Charge is Stripe's fundamental revenue object. Every successful payment—card, ACH, ACM—creates a charge. Each charge has an amount, currency, status, and created timestamp. When you see revenue metrics in Stripe's Dashboard, they're aggregated from charges. Most integrations start by querying charges.
const stripe = require('stripe')('sk_test_...');
// List charges from the last 30 days
const charges = await stripe.charges.list({
limit: 100,
created: {
gte: Math.floor(Date.now() / 1000) - 86400 * 30
}
});
const totalRevenue = charges.data.reduce((sum, charge) => sum + charge.amount, 0);
console.log(`Total: $${totalRevenue / 100} (amount in cents)`);
Gross vs. Net: Understanding the Gap
Stripe reports gross revenue (what customers paid) in the Dashboard. But your actual revenue is net (gross minus Stripe's fee). For card charges, Stripe takes 2.9% + $0.30. Your bank account only receives the net amount. This is the #1 reconciliation headache.
// Get what you actually received (net revenue)
const balance = await stripe.balance.retrieve();
const available = balance.available[0]; // ready to payout
const pending = balance.pending[0]; // settling in 1-2 days
console.log(`Available balance: $${available.amount / 100}`);
console.log(`Pending: $${pending.amount / 100}`);
console.log(`Total: $${(available.amount + pending.amount) / 100}`);
Status Matters: Succeeded vs. Failed vs. Refunded
Successful charges contribute to revenue. Failed charges don't. Refunded charges reduce revenue. Stripe webhooks and the Charges API both track status changes. If you don't filter for status: 'succeeded', you'll overcount revenue.
// Only count succeeded charges
const succeededCharges = await stripe.charges.list({
status: 'succeeded',
limit: 100
});
// Count refunded charges
const refundedCharges = await stripe.charges.list({
limit: 100
});
const refundedAmount = refundedCharges.data
.filter(c => c.refunded === true)
.reduce((sum, c) => sum + c.amount_refunded, 0);
console.log(`Refunded: $${refundedAmount / 100}`);
Accessing Revenue Data
Three ways to get revenue data: the Dashboard, API queries, and webhooks. Each has trade-offs.
Use the Dashboard for Quick Snapshots
Go to Reporting → Payments for transaction-level details or Balance for your current settlement. Payouts shows when money reaches your bank account. These views are fast but not suitable for automation or large-scale analysis.
// Automate Dashboard checks with API queries
const charges = await stripe.charges.list({
limit: 100,
created: {
gte: Math.floor(Date.now() / 1000) - 86400 // last 24 hours
}
});
console.log(`Charges in last 24h: ${charges.data.length}`);
Query Charges for Custom Reports
The Charges API lets you filter by date, customer, status, amount range, and payment method. Build revenue reports, reconciliation tools, or customer lifetime value calculations by querying charges. This is how most finance integrations work.
// Revenue by customer in last quarter
const charges = await stripe.charges.list({
limit: 100,
status: 'succeeded',
created: {
gte: Math.floor(new Date('2025-01-01').getTime() / 1000),
lte: Math.floor(new Date('2025-03-31').getTime() / 1000)
}
});
const revenueByCustomer = {};
charges.data.forEach(charge => {
const customerId = charge.customer;
revenueByCustomer[customerId] = (revenueByCustomer[customerId] || 0) + charge.amount;
});
console.log('Customer revenue:', revenueByCustomer);
Listen to Webhooks for Real-Time Tracking
Webhooks fire when charges succeed, fail, or are refunded. Use webhooks to update your own database or send data to accounting software in real time, rather than batch-querying charges. This is essential for accuracy-critical systems.
// Webhook endpoint to track revenue in real time
const express = require('express');
const app = express();
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec_test_...'
);
if (event.type === 'charge.succeeded') {
const charge = event.data.object;
console.log(`New revenue: $${charge.amount / 100}`);
}
if (event.type === 'charge.refunded') {
const charge = event.data.object;
console.log(`Refund: $${charge.amount_refunded / 100}`);
}
res.json({received: true});
});
Common Pitfalls
- Confusing gross and net. Dashboard shows gross; your bank gets net. This is the #1 reconciliation mistake.
- Not filtering by status.
stripe.charges.list()returns all charges. You needstatus: 'succeeded'to see actual revenue. - Assuming batch queries catch everything. Stripe's list API paginates at 100 items; use
starting_afterto iterate through all charges. - Relying on webhooks alone. Webhooks can fail or be delayed. Always verify with periodic API queries.
Wrapping Up
Revenue reporting in Stripe comes down to three tools: the Dashboard for quick checks, the Charges API for custom reporting, and webhooks for real-time tracking. Most robust systems use all three. If you want to track revenue automatically across multiple tools and create unified financial dashboards, Product Analyst can help.