6 min read

How to Set Up Revenue Recognition in Stripe

Most SaaS businesses don't recognize revenue the moment payment hits the bank—you need to recognize it as you deliver service. Stripe Billing handles the transaction mechanics, but you need to wire up webhooks and event tracking to capture revenue at the right moment. Let's show you how.

Understanding When Revenue is Recognized

Stripe gives you three moments to hook into: invoice finalized, invoice paid, or subscription activated. Pick the right one based on your accounting rules.

Define Your Revenue Recognition Trigger

In Stripe, revenue recognition typically happens when an invoice status changes, not when the payment settles. For most SaaS, you recognize revenue when the invoice is finalized or paid. If you're selling annual plans, you'll recognize revenue over 12 months using subscription schedules and tracking the invoice creation date.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Retrieve an invoice to check its status
const invoice = await stripe.invoices.retrieve('in_xxxxx');
console.log(`Invoice ${invoice.id} status: ${invoice.status}`);
// Status can be 'draft', 'open', 'paid', 'uncollectible', 'void'

// List invoices for a subscription to build revenue history
const invoices = await stripe.invoices.list({
  subscription: 'sub_xxxxx',
  limit: 100
});
invoices.data.forEach(inv => {
  const paidDate = inv.status_transitions.paid_at ? new Date(inv.status_transitions.paid_at * 1000) : null;
  console.log(`Invoice ${inv.id}: ${inv.amount_paid / 100} USD on ${paidDate}`);
});
Check invoice status and track revenue events from your invoice history

Set Up Webhook Endpoints for Revenue Events

Go to your Stripe dashboard Developers > Webhooks and create a new endpoint. Add invoice.finalized and invoice.paid events. These events fire when Stripe moves the invoice through its lifecycle, giving you the signals you need to recognize revenue.

javascript
const express = require('express');
const app = express();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } 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 for invoice ${invoice.id}: ${invoice.amount_due / 100} USD`);
    // INSERT INTO revenue_recognition_ledger ...
  }

  if (event.type === 'invoice.paid') {
    const invoice = event.data.object;
    console.log(`Payment confirmed for invoice ${invoice.id}`);
  }

  res.json({received: true});
});

app.listen(3000);
Webhook handler to capture invoice state changes and trigger revenue recognition logic
Tip: Don't rely on charge.succeeded for revenue recognition—invoices can fail after payment settles. Use invoice events instead.

Handling Multi-Month Revenue with Subscription Schedules

For annual or multi-month plans, you need to split revenue recognition across periods. Stripe Billing supports this through subscription schedules and the billing_cycle_anchor field.

Create a Subscription with Billing Cycle Anchor

When you create a subscription, set the billing_cycle_anchor to control when billing cycles start. This matters for revenue recognition—you want each period's revenue recognized at period start or invoice finalization, not all at once.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Create annual subscription with monthly revenue recognition
const subscription = await stripe.subscriptions.create({
  customer: 'cus_xxxxx',
  items: [
    {
      price: 'price_xxxxx',
      quantity: 1
    }
  ],
  billing_cycle_anchor: Math.floor(Date.now() / 1000),
  expand: ['latest_invoice.payment_intent']
});

console.log(`Subscription created: ${subscription.id}`);
console.log(`Next billing: ${new Date(subscription.current_period_end * 1000)}`);

// For monthly revenue recognition of annual plans, use subscription schedules
const schedule = await stripe.subscriptionSchedules.create({
  customer: 'cus_xxxxx',
  phases: [
    {
      items: [
        {
          price: 'price_xxxxx',
          quantity: 1
        }
      ],
      iterations: 12,
      duration: { interval: 'month', interval_count: 1 }
    }
  ]
});

console.log(`Schedule created: ${schedule.id}`);
// Stripe auto-generates 12 invoices, one per month
Use billing_cycle_anchor and subscription schedules to align revenue recognition with your accounting periods

Query Invoices by Date for Reporting

Pull invoices within a specific date range to build your revenue recognition reports. Filter by created timestamp and invoice status to match your accounting period cutoffs.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const startOfMonth = Math.floor(new Date('2024-03-01').getTime() / 1000);
const endOfMonth = Math.floor(new Date('2024-03-31').getTime() / 1000);

// Get all invoices created in March 2024
const invoices = await stripe.invoices.list({
  created: {
    gte: startOfMonth,
    lte: endOfMonth
  },
  limit: 100,
  status: 'paid'
});

let totalRevenue = 0;
invoices.data.forEach(invoice => {
  const recognizedAmount = invoice.amount_paid / 100;
  totalRevenue += recognizedAmount;
  console.log(`Customer ${invoice.customer}: $${recognizedAmount.toFixed(2)}`);
});

console.log(`Total recognized revenue for March: $${totalRevenue.toFixed(2)}`);
Watch out: If you use automatic_tax or coupons, the amount_due may differ from the base price. Use amount_paid for actual revenue recognized.

Syncing Revenue Data to Your Analytics

Once you're capturing invoice events, push the data to your data warehouse so you can track MRR, ARR, and cohort revenue.

Transform and Load Invoice Data

Map Stripe invoice fields to your schema: id, customer, subscription, amount_paid, period_start, period_end, status. Filter out failed or voided invoices before loading into your database.

javascript
const transformInvoiceToRevenueRecord = (invoice) => {
  return {
    stripe_invoice_id: invoice.id,
    stripe_subscription_id: invoice.subscription,
    stripe_customer_id: invoice.customer,
    amount_recognized_cents: invoice.amount_paid,
    currency: invoice.currency,
    period_start: new Date(invoice.period_start * 1000),
    period_end: new Date(invoice.period_end * 1000),
    invoice_date: new Date(invoice.created * 1000),
    status: invoice.status,
    is_paid: invoice.paid,
    is_void: invoice.status === 'void'
  };
};

// In your webhook handler:
if (event.type === 'invoice.finalized') {
  const record = transformInvoiceToRevenueRecord(event.data.object);
  // INSERT INTO revenue_recognition_ledger VALUES (...)
  console.log(`Inserting revenue record:`, record);
}

if (event.type === 'invoice.paid') {
  console.log(`Invoice ${event.data.object.id} marked as paid`);
}
Standardize Stripe invoice data into your revenue ledger format

Common Pitfalls

  • Recognizing revenue on payment_intent.succeeded instead of invoice.paid—a payment can succeed but the invoice may be disputed, refunded, or voided later.
  • Forgetting to exclude test invoices—use livemode filter when querying to avoid mixing test data with production revenue.
  • Not handling refunds—when an invoice is refunded, Stripe creates a credit note. You need separate logic to reverse revenue recognition.
  • Using amount_due instead of amount_paid—taxes, coupons, and credits change the recognized amount; always use actual paid amount.

Wrapping Up

You now have the wiring in place to recognize revenue the moment Stripe creates or finalizes invoices. Use subscription schedules for multi-month plans, webhooks for real-time signals, and invoice queries for period-end reporting. If you want to track this automatically across tools, Product Analyst can help.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free