Stripe doesn't calculate LTV for you—you have to pull subscription and payment data yourself. But that's actually good news: you can set up webhooks to trigger alerts whenever a customer's lifetime value hits a milestone or changes significantly. This guide shows you how.
Create a Webhook Endpoint to Listen for Subscription Events
Webhooks let Stripe notify your app whenever something relevant happens (subscription created, updated, payment received). You'll listen for these events and calculate LTV on the fly.
Create an endpoint to receive webhook events
Set up an HTTP endpoint that Stripe can POST to. You'll verify the signature to make sure the event is legit, then process it. Use your Secret Key from the Stripe Dashboard to verify signatures.
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
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, webhookSecret);
} catch (err) {
console.error(`Webhook signature verification failed: ${err.message}`);
return res.sendStatus(400);
}
if (['customer.subscription.updated', 'invoice.payment_succeeded'].includes(event.type)) {
const customerId = event.data.object.customer;
const ltv = await calculateCustomerLTV(customerId);
await checkLTVAlert(customerId, ltv);
}
res.json({received: true});
});
app.listen(3000, () => console.log('Webhook server running'));Register your endpoint in the Stripe Dashboard
Go to Developers > Webhooks in the Stripe Dashboard. Click Add endpoint and paste your webhook URL. Select customer.subscription.updated, invoice.payment_succeeded, and charge.refunded to catch all LTV changes.
Query Stripe to Calculate Each Customer's Total Revenue
Once you know which customer changed, fetch their subscription and invoice history to sum up total lifetime revenue.
Fetch all paid invoices for the customer
Use stripe.invoices.list() to get all invoices for the customer. Sum the amount_paid field to get total revenue. Filter by status: 'paid' to exclude drafts and failed charges.
async function calculateCustomerLTV(customerId) {
const invoices = await stripe.invoices.list({
customer: customerId,
limit: 100,
status: 'paid'
});
let totalRevenue = 0;
invoices.data.forEach(invoice => {
totalRevenue += invoice.amount_paid;
});
return totalRevenue / 100; // Convert from cents to dollars
}Handle refunds separately
A refunded charge lowers LTV but status: 'paid' only includes successfully collected invoices. Track charge.refunded events in your webhook to subtract refunded amounts from your cached LTV. Watch out: If a customer disputes a charge, the refund may come weeks later—your alert logic needs to account for that lag.
Create Alert Logic Based on LTV Thresholds
Now that you have the LTV, define when to alert. Common triggers: LTV crosses a milestone, LTV drops suddenly, or a high-value customer arrives.
Set up alert conditions in your webhook handler
After calculating LTV, compare it to thresholds. Send alerts via email, Slack, or your internal system. Track both absolute thresholds (e.g., over $5000) and relative changes (e.g., dropped 20%) to catch both high-value wins and churn signals.
async function checkLTVAlert(customerId, ltv) {
const ALERT_THRESHOLD = 5000;
const CHURN_ALERT_THRESHOLD = 0.8;
const previousLTV = await redis.get(`ltv_${customerId}`) || 0;
const percentChange = previousLTV > 0 ? (ltv - previousLTV) / previousLTV : 0;
if (ltv > ALERT_THRESHOLD) {
await sendAlert(`High-value customer: ${customerId} reached $${ltv.toFixed(2)} LTV`);
}
if (percentChange < -CHURN_ALERT_THRESHOLD && previousLTV > 0) {
await sendAlert(`Churn warning: ${customerId} LTV dropped from $${previousLTV} to $${ltv}`);
}
await redis.set(`ltv_${customerId}`, ltv);
}
async function sendAlert(message) {
await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
method: 'POST',
body: JSON.stringify({ text: message })
});
}Handle pagination for high-invoice customers
Stripe paginates invoice lists in batches of 100. If a customer has hundreds of invoices, you need to loop through all pages using the starting_after parameter to get the complete LTV, not just the first 100.
Common Pitfalls
- Forgetting pagination—customers with many invoices will have incomplete LTV if you only fetch the first batch of 100. Loop through all pages.
- Counting refunded charges as revenue—always filter for
status: 'paid'to exclude refunds and drafts. - Querying Stripe on every webhook instead of caching—you'll hit rate limits and slow down alerts. Cache LTV locally and update incrementally.
- Ignoring the time lag on refunds—a refund event may come weeks after the original charge. Your alert logic needs to account for delayed adjustments to LTV.
Wrapping Up
You now have real-time LTV alerts by combining Stripe webhooks with invoice queries and your own alert logic. Store the results in your database so you can track trends over time and find your most valuable customers before they churn. If you want to track this automatically across tools, Product Analyst can help.