5 min read

How to Monitor LTV in Stripe

Stripe doesn't calculate LTV for you—you need to query your payment data and run the math yourself. If you're tracking revenue health and customer value, LTV is essential, but it's hidden in your customer records until you pull it out.

Retrieve All Customer Payments

LTV starts with knowing what a customer has actually paid. Stripe stores every charge, refund, and invoice, so you can reconstruct the full payment history.

Step 1: Install the Stripe Node SDK

Add the official Stripe library to your project. Use npm install stripe or pnpm add stripe depending on your package manager.

javascript
npm install stripe
Install the Stripe Node.js SDK

Step 2: Query a customer's charges

Use the Charges API to fetch all successful payments for a specific customer. The stripe.charges.list() method returns paginated results, so you'll need to handle pagination if a customer has many transactions.

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

async function getCustomerCharges(customerId) {
  let allCharges = [];
  let hasMore = true;
  let startingAfter = null;

  while (hasMore) {
    const charges = await stripe.charges.list({
      customer: customerId,
      limit: 100,
      starting_after: startingAfter,
    });

    allCharges = allCharges.concat(charges.data);
    hasMore = charges.has_more;
    if (hasMore) {
      startingAfter = charges.data[charges.data.length - 1].id;
    }
  }

  return allCharges;
}
Fetch all charges for a customer, handling pagination

Step 3: Filter for successful payments

Charge objects include failed attempts and refunds. Filter to only count status: 'succeeded' to get real revenue that actually hit your account.

javascript
const successfulCharges = allCharges.filter(
  charge => charge.status === 'succeeded' && !charge.refunded
);

const totalInCents = successfulCharges.reduce((sum, c) => sum + c.amount, 0);
const totalInDollars = totalInCents / 100;

console.log(`Found ${successfulCharges.length} successful charges`);
console.log(`Total revenue: $${totalInDollars.toFixed(2)}`);
Filter for succeeded charges and sum the amounts
Watch out: Charges include subscription payments and one-off transactions mixed together. Check the invoice field—if it's populated, that's a subscription payment. If null, it's a one-off charge.

Calculate Customer Lifetime Value

LTV is the total net amount a customer has paid you. Stripe doesn't give you this directly, so you calculate it from successful charges minus refunds.

Step 1: Sum charges and subtract refunds

Stripe tracks refunds separately. For each charge, add the amount, then subtract any refunded amounts using the refunds array on the charge object.

javascript
function calculateLTV(charges) {
  let totalRevenue = 0;

  charges.forEach(charge => {
    if (charge.status === 'succeeded') {
      totalRevenue += charge.amount;
      
      if (charge.refunded && charge.refunds.data.length > 0) {
        charge.refunds.data.forEach(refund => {
          totalRevenue -= refund.amount;
        });
      }
    }
  });

  return totalRevenue / 100; // Convert from cents to dollars
}

const ltv = calculateLTV(successfulCharges);
console.log(`Customer LTV: $${ltv.toFixed(2)}`);
Calculate LTV by summing charges and subtracting refunds

Step 2: Split LTV by payment type

If you want to track recurring vs. one-time revenue separately, filter charges by the invoice field. Charges with an invoice ID are subscription-related; those without are one-offs.

javascript
const subscriptionCharges = allCharges.filter(c => c.invoice);
const oneOffCharges = allCharges.filter(c => !c.invoice);

const subscriptionLTV = calculateLTV(subscriptionCharges);
const oneOffLTV = calculateLTV(oneOffCharges);

console.log(`Subscription LTV: $${subscriptionLTV.toFixed(2)}`);
console.log(`One-off LTV: $${oneOffLTV.toFixed(2)}`);
console.log(`Total LTV: $${(subscriptionLTV + oneOffLTV).toFixed(2)}`);
Separate LTV by recurring vs. one-time payments
Tip: For subscription customers, you can calculate *expected* lifetime value by looking at MRR and subscription age, but stick with actual charges for a true LTV number. Expected LTV is useful for cohort analysis, but it's a forecast, not reality.

Automate LTV Monitoring

Calculating LTV once is useful, but you need to track it over time. Set up a webhook listener that updates LTV whenever a charge succeeds.

Step 1: Create a reusable LTV function

Wrap your LTV calculation in a function that takes a customer ID and returns the current LTV. This is what you'll call from webhooks or scheduled jobs.

javascript
async function getLTVForCustomer(customerId) {
  const charges = await getCustomerCharges(customerId);
  return calculateLTV(charges);
}

module.exports = { getLTVForCustomer };
Export the LTV calculation as a reusable function

Step 2: Listen for charge.succeeded webhooks

Use Stripe Webhooks to listen for charge.succeeded events. When a charge goes through, recalculate and store the updated LTV. Set up your endpoint in the Stripe Dashboard under Developers > Webhooks.

javascript
const express = require('express');
const app = express();
const { getLTVForCustomer } = require('./ltv');

app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (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 === 'charge.succeeded') {
    const customerId = event.data.object.customer;
    const ltv = await getLTVForCustomer(customerId);
    console.log(`Customer ${customerId} LTV: $${ltv.toFixed(2)}`);
    
    // Store in your database here
  }

  res.json({received: true});
});
Listen for charge.succeeded webhooks and recalculate LTV

Step 3: Persist LTV snapshots for trending

Save each LTV calculation (customer ID, amount, timestamp) to your own database or metrics backend. This lets you track how LTV changes over time and identify your best customers.

javascript
// After calculating LTV from a webhook event
async function saveLTVSnapshot(customerId, ltv) {
  await db.query(
    'INSERT INTO customer_ltv (customer_id, ltv_amount, recorded_at) VALUES ($1, $2, $3)',
    [customerId, ltv, new Date()]
  );
  
  console.log(`Saved LTV for ${customerId}: $${ltv.toFixed(2)}`);
}

// Call after every charge.succeeded
await saveLTVSnapshot(customerId, ltv);
Store LTV snapshots for historical tracking
Tip: If you have thousands of customers, querying all charges for all customers on a schedule is expensive. Use webhooks to update LTV incrementally as charges occur instead.

Common Pitfalls

  • Forgetting to filter for status: 'succeeded'—failed charges will artificially lower your LTV if included.
  • Not accounting for refunds—a large refund that you ignore will make LTV wrong and your reporting unreliable.
  • Mixing subscription and one-off revenue—these represent different customer segments and need separate tracking for meaningful analysis.
  • Querying all customers at once on a schedule—you'll hit Stripe API rate limits. Use webhooks instead to update LTV as payments occur.

Wrapping Up

LTV in Stripe requires you to query and aggregate your own data, but it's straightforward once you know where to look. Pull charges from the API, filter for successes, subtract refunds, and set up webhooks to track changes automatically. If you want to track this automatically across tools and integrate it with your other product metrics, Product Analyst can help.

Track these metrics automatically

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

Try Product Analyst — Free