Stripe doesn't calculate LTV for you—but your payment data is already there. You need to know which customers are actually worth your attention, and that means pulling subscription revenue, historical payments, and churn signals into a single view. Here's how to build that.
Store LTV as customer metadata
The simplest approach: calculate LTV once, store it on the customer object, and update it as revenue changes.
Create a customer with LTV metadata
When you onboard a customer or process their first payment, add an ltv field to their metadata. This makes it queryable later and keeps all customer data in one place.
const stripe = require('stripe')('sk_live_...');
const customer = await stripe.customers.create({
email: '[email protected]',
name: 'Jane Doe',
metadata: {
ltv: '0',
signup_date: new Date().toISOString(),
account_type: 'premium'
}
});Update LTV after calculating revenue
Once you've summed up paid invoices for a customer, push that back to Stripe. Use the Customers page in the Stripe Dashboard to see metadata in real-time.
await stripe.customers.update(customerId, {
metadata: {
ltv: '1250.00',
ltv_last_updated: new Date().toISOString(),
churn_risk: 'low'
}
});Query and calculate LTV from subscription data
Stripe subscriptions are the source of truth for recurring revenue. Pull subscription status, monthly charges, and invoice totals to calculate real LTV.
Fetch all subscriptions for a customer
List all active and past subscriptions to understand revenue history. The subscription object includes current_period_start, current_period_end, and plan amount.
const subscriptions = await stripe.subscriptions.list({
customer: customerId,
status: 'all'
});
let totalMrr = 0;
subscriptions.data.forEach(sub => {
if (sub.status === 'active') {
totalMrr += sub.plan.amount / 100;
}
console.log(`Plan: $${sub.plan.amount / 100}, Status: ${sub.status}`);
});Sum invoice totals to get cumulative revenue
Invoices represent actual money collected. Query by customer and status to get paid invoices, then total the amounts.
const invoices = await stripe.invoices.list({
customer: customerId,
status: 'paid',
limit: 100
});
let totalRevenue = 0;
invoices.data.forEach(invoice => {
totalRevenue += invoice.amount_paid;
});
const ltv = (totalRevenue / 100).toFixed(2);
console.log(`Customer LTV: $${ltv}`);Account for refunds and adjustments
Paid invoices don't tell the full story if the customer has refunds. Query charges to see the net amount after refunds are applied.
const charges = await stripe.charges.list({
customer: customerId,
limit: 100
});
let netRevenue = 0;
charges.data.forEach(charge => {
const chargeNet = charge.amount_captured - charge.amount_refunded;
netRevenue += chargeNet;
});
const adjustedLtv = (netRevenue / 100).toFixed(2);
console.log(`LTV after refunds: $${adjustedLtv}`);Automate LTV updates with webhooks
Manual queries work once, but LTV changes constantly. Set up webhooks to recalculate and update LTV whenever a customer makes a payment, cancels, or refunds.
Listen for subscription and payment events
Create a webhook endpoint that responds to invoice.paid, charge.refunded, and customer.subscription.deleted. These events fire every time revenue changes.
const express = require('express');
const app = express();
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec_live_...'
);
if (['invoice.paid', 'charge.refunded', 'customer.subscription.deleted'].includes(event.type)) {
const customerId = event.data.object.customer;
await updateCustomerLtv(customerId);
}
res.status(200).json({received: true});
});Recalculate LTV inside your webhook handler
When a webhook fires, query the customer's invoices again, recalculate LTV, and update metadata. This keeps Stripe in sync with your latest numbers.
async function updateCustomerLtv(customerId) {
const invoices = await stripe.invoices.list({
customer: customerId,
status: 'paid'
});
const ltv = invoices.data.reduce((sum, inv) => sum + inv.amount_paid, 0) / 100;
await stripe.customers.update(customerId, {
metadata: {
ltv: ltv.toFixed(2),
ltv_last_updated: new Date().toISOString()
}
});
console.log(`Updated LTV for ${customerId}: $${ltv.toFixed(2)}`);
}Register your webhook in the Dashboard
Go to Developers > Webhooks in the Stripe Dashboard, add your endpoint URL, and select the events: invoice.paid, charge.refunded, and customer.subscription.deleted.
Common Pitfalls
- Forgetting to account for refunds—LTV based on invoice totals alone overstates customer value when refunds occur.
- Querying without pagination limits—Stripe returns up to 100 items per request; for long customer histories, you'll miss data without looping through all pages.
- Storing LTV in metadata then never updating it—metadata is a snapshot, not real-time; you need webhooks to keep it current.
- Calculating LTV as MRR × expected months—subscription cancellations are unpredictable; actual LTV is historical revenue, not a projection.
Wrapping Up
You now have LTV stored on every customer in Stripe and a webhook that updates it automatically. The next step is connecting this to churn prediction or cohort analysis—seeing which LTV tiers are at risk of canceling, or how LTV changes over time. If you want to track this automatically across tools and connect it to your product metrics, Product Analyst can help.