Revenue recognition isn't when you get paid—it's when you've actually earned the money. In Stripe, subscriptions and invoices don't automatically tell you when revenue is recognized; you need to capture the right events and timestamps. This guide shows you how to track revenue recognition properly in Stripe instead of guessing based on payment dates.
Capture Invoice Events via Webhooks
The foundation of revenue tracking in Stripe is listening to invoice lifecycle events. You need to know when an invoice is finalized (recognized), not just when it's paid.
Set up a webhook endpoint for invoice events
Create an endpoint that receives Stripe webhook events. Focus on invoice.finalized (when revenue is recognized) and invoice.payment_succeeded (when payment clears). Use the Stripe CLI locally to test webhooks before deploying.
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'invoice.finalized') {
const invoice = event.data.object;
console.log(`Revenue recognized: $${(invoice.amount_due / 100).toFixed(2)}`);
}
res.status(200).json({received: true});
});Map revenue to the subscription period, not payment date
For subscriptions, revenue is recognized at the start of each billing period. Use period_start from the invoice object as your revenue recognition date, not the payment timestamp.
if (event.type === 'invoice.finalized') {
const invoice = event.data.object;
const periodStart = new Date(invoice.period_start * 1000);
console.log(`Invoice finalized for period: ${periodStart.toISOString().split('T')[0]}`);
// Record revenue on period start, not when paid
sendToAccounting({
amount: invoice.amount_due / 100,
recognitionDate: periodStart,
invoiceId: invoice.id
});
}invoice.created fires before finalization. Use invoice.finalized to get the actual revenue recognition timestamp.Add Metadata to Segment Revenue
Stripe webhooks give you the invoice, but metadata helps you add business context—customer segment, product tier, cost center—so you can slice revenue later without rebuilding data.
Attach metadata when creating subscriptions
Include metadata fields on subscriptions so they flow through to invoices automatically. This tags revenue at the source without requiring separate lookup tables.
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{
price: 'price_1234567890',
}],
metadata: {
customerSegment: 'enterprise',
productTier: 'pro',
costCenter: 'sales'
}
});Filter invoices by metadata in reports
Use the Stripe API to retrieve and aggregate invoices by your custom metadata fields. This lets you segment revenue without joining against your own database.
const invoices = await stripe.invoices.list({
limit: 100,
status: 'paid'
});
const enterpriseRevenue = invoices.data
.filter(inv => inv.subscription)
.map(inv => {
const sub = await stripe.subscriptions.retrieve(inv.subscription);
return {
amount: inv.amount_paid / 100,
segment: sub.metadata?.customerSegment,
date: new Date(inv.period_start * 1000).toISOString().split('T')[0]
};
})
.filter(item => item.segment === 'enterprise')
.reduce((sum, item) => sum + item.amount, 0);
console.log(`Enterprise revenue: $${enterpriseRevenue.toFixed(2)}`);Track Adjustments and Refunds
Credits, discounts, and refunds reduce the revenue you actually recognize. You need to track these as adjustments to the original period, not separate transactions.
Capture invoice adjustments before finalization
Listen to invoice.updated to catch when credits or discounts are applied. Use the adjusted amount_due, not the original amount, as your revenue figure.
if (event.type === 'invoice.updated') {
const invoice = event.data.object;
const prevInvoice = event.data.previous_attributes;
if (prevInvoice.amount_due && prevInvoice.amount_due !== invoice.amount_due) {
const creditApplied = (prevInvoice.amount_due - invoice.amount_due) / 100;
console.log(`Invoice adjusted. Credit: $${creditApplied.toFixed(2)}`);
// Update revenue to the adjusted amount
updateRevenue(invoice.id, invoice.amount_due);
}
}Record refunds as negative revenue in the original period
When a refund is issued, create a negative revenue entry for the original billing period. Use the refund timestamp and original invoice's period start to match it correctly.
if (event.type === 'charge.refunded') {
const charge = event.data.object;
const invoices = await stripe.invoices.list({
customer: charge.customer,
limit: 10
});
const originalInvoice = invoices.data.find(inv =>
inv.charge === charge.id || inv.payment_intent === charge.payment_intent
);
if (originalInvoice) {
console.log(`Refund: -$${(charge.amount_refunded / 100).toFixed(2)} for period ${new Date(originalInvoice.period_start * 1000).toISOString().split('T')[0]}`);
// Record as negative revenue in the original period
sendToAccounting({
amount: -(charge.amount_refunded / 100),
recognitionDate: new Date(originalInvoice.period_start * 1000),
invoiceId: originalInvoice.id,
type: 'refund'
});
}
}Common Pitfalls
- Using
invoice.paidor payment timestamp instead ofinvoice.finalized—revenue is recognized when Stripe finalizes the invoice, not when money lands. - Treating invoice amount as revenue without accounting for credits, discounts, and prior balance adjustments that reduce the final amount.
- Ignoring subscription period dates and using invoice creation or payment dates—revenue is recognized on
period_start, not payment date. - Not tracking refunds as adjustments to the original period—refunds are negative revenue entries tied to the billing period they reverse, not standalone events.
Wrapping Up
Revenue recognition in Stripe requires capturing the right events (invoice.finalized), using correct timestamps (period_start for subscriptions), and tracking adjustments (credits, refunds) as separate entries. This gives you a single source of truth for revenue timing that matches accounting standards. If you want to track this automatically across tools, Product Analyst can help.