Revenue recognition sounds like accounting jargon, but it directly impacts how you report financial health. In Stripe, revenue is recognized when a payment is confirmed—not when an invoice is created. This matters for SaaS businesses with subscriptions, annual plans, and refunds.
What Revenue Recognition Means
Revenue recognition is the moment you officially record money as earned income on your books. Stripe gives you the events to mark that moment exactly.
Recognize Revenue on Payment Confirmation
Revenue is recognized when a payment transitions to succeeded status, not before. If you're using Payment Intents or Charges, listen for the payment_intent.succeeded webhook event. For subscriptions, track invoice.payment_succeeded instead.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', (req, res) => {
const event = req.body;
if (event.type === 'payment_intent.succeeded') {
const paymentIntent = event.data.object;
console.log(`Revenue recognized: ${paymentIntent.amount} cents`);
// Write to your revenue ledger
}
if (event.type === 'invoice.payment_succeeded') {
const invoice = event.data.object;
console.log(`Subscription payment recognized: ${invoice.amount_paid} cents`);
// Process subscription revenue
}
res.json({ received: true });
});Distinguish Between Payment Types
Subscription revenue (monthly, annual) and one-time payments need different recognition logic. Subscriptions use invoice.payment_succeeded; one-time payments use payment_intent.succeeded or charge.succeeded. Always check the object.status field to confirm the payment actually succeeded.
app.post('/webhook', async (req, res) => {
const event = req.body;
if (event.type === 'invoice.payment_succeeded') {
const invoice = event.data.object;
const isSubscription = invoice.subscription !== null;
const amount = invoice.amount_paid;
if (isSubscription) {
// Recurring revenue - recognize for this billing cycle only
recordSubscriptionRevenue(invoice.subscription, amount, invoice.period_start);
} else {
// One-time invoice
recordOneTimeRevenue(invoice.customer, amount);
}
}
res.json({ received: true });
});Account for Failed and Refunded Payments
Revenue recognition isn't final until disputes and refunds are resolved. Listen to charge.refunded and charge.dispute.created events to reverse previously recognized revenue. If a charge fails immediately, payment_intent.payment_failed means no revenue to recognize.
if (event.type === 'charge.refunded') {
const charge = event.data.object;
const refundedAmount = charge.amount_refunded;
// Reverse the revenue recognition
reverseRevenue(charge.invoice, refundedAmount);
}
if (event.type === 'charge.dispute.created') {
const dispute = event.data.object;
// Flag this revenue as contested, might need reversal
holdRevenue(dispute.charge);
}created, paid, and period_start/period_end timestamps on invoices. Use these to match revenue to the correct accounting period.Building Revenue Recognition into Your Accounting
Once you recognize revenue in Stripe, you need to feed it into your accounting system. This is where Stripe's data structure matters.
Extract Detailed Revenue Data from Events
Every payment_intent.succeeded or invoice.payment_succeeded event contains metadata you need: customer ID, line items, tax, discounts, currency. Use the webhook event to build your revenue record, not the dashboard totals.
if (event.type === 'invoice.payment_succeeded') {
const invoice = event.data.object;
const revenueRecord = {
stripeInvoiceId: invoice.id,
customerId: invoice.customer,
amount: invoice.amount_paid,
subtotal: invoice.subtotal,
tax: invoice.tax,
discount: invoice.total_discount_amounts
.reduce((sum, d) => sum + d.amount, 0),
currency: invoice.currency,
recognizedAt: new Date(invoice.paid * 1000),
items: invoice.lines.data.map(line => ({
description: line.description,
amount: line.amount,
period: { start: line.period?.start, end: line.period?.end }
}))
};
saveToLedger(revenueRecord);
}
Handle Multi-Currency and Local Taxes
Stripe reports amounts in the customer's currency and includes tax details per jurisdiction. When recognizing revenue, convert to your base currency and respect tax liability separately. The tax field on invoices is pre-calculated; don't double-count.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Retrieve full invoice details for revenue recognition
const invoice = await stripe.invoices.retrieve(invoiceId);
const revenueInBaseCurrency = convertCurrency(
invoice.amount_paid,
invoice.currency,
'USD'
);
const taxLiability = invoice.tax || 0;
const netRevenue = revenueInBaseCurrency - convertCurrency(taxLiability, invoice.currency, 'USD');
// Net revenue is what goes to revenue recognition; tax is a liability
recognizeRevenue(netRevenue, 'USD');
recordTaxLiability(taxLiability, invoice.currency);Common Pitfalls
- Recognizing revenue when invoices are created instead of when they're paid. Stripe invoices stay in draft or pending until payment succeeds—that's when revenue should be recognized.
- Forgetting to reverse revenue when refunds, chargebacks, or disputes occur. Stripe sends separate events for these; you must listen to them and adjust your ledger.
- Mixing up net revenue and gross revenue. Stripe reports tax separately; your revenue number shouldn't include tax amounts if your accounting standards require net figures.
- Using dashboard totals instead of webhook events. The dashboard rounds and aggregates; webhooks give you exact, timestamped detail needed for audit trails.
Wrapping Up
Revenue recognition in Stripe happens when payments confirm, not when invoices appear. Build webhook listeners for payment_intent.succeeded, invoice.payment_succeeded, and refund events to catch every revenue transaction with full detail. If you want to track revenue recognition automatically across Stripe and other payment tools, Product Analyst can help.