5 min read

What Is Revenue Recognition in Stripe

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.

javascript
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 });
});
Listen for succeeded events to mark revenue as recognized

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.

javascript
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 });
});
Route revenue differently for subscriptions vs one-time payments

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.

javascript
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);
}
Adjust revenue when payments are reversed or disputed
Tip: Stripe includes 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.

javascript
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);
}
Structure revenue data from Stripe events for accounting integration

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.

javascript
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);
Separate revenue from tax liability when currencies differ
Watch out: Stripe's dashboard shows amounts in the payment currency, not your reporting currency. Always use the API to get consistent decimal precision and convert programmatically.

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.

Track these metrics automatically

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

Try Product Analyst — Free