You're spending money on ads, but when does that investment actually pay off? CAC payback is when a customer's lifetime value covers what you spent to acquire them. In Stripe, this means connecting your marketing costs to the revenue each customer generates—and Stripe's APIs and webhooks make this surprisingly tractable.
Tag Customers with Acquisition Metadata
Every customer who converts should carry metadata about how you acquired them—campaign, cost, date. Stripe's customer metadata is the place to store this.
Store acquisition cost and source on customer creation
When a customer signs up, create a Stripe customer with metadata that includes acquisition cost, campaign source, and conversion date. This becomes the baseline for your CAC payback calculation.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const customer = await stripe.customers.create({
email: '[email protected]',
metadata: {
acquisition_cost: '50', // Cost paid to acquire this customer (e.g., $50 ad spend)
acquisition_source: 'google_ads',
acquisition_date: new Date().toISOString(),
campaign_id: 'campaign_12345'
}
});Use webhooks to capture customer signup in real time
Listen for customer.created events to log when a conversion happens. This ensures acquisition data is captured the moment the customer is created.
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
if (event.type === 'customer.created') {
const customer = event.data.object;
console.log(`Customer ${customer.id} acquired:`, customer.metadata);
// Log to your analytics backend
}
res.json({received: true});
});acquisition_cost as a string in metadata (Stripe only accepts strings), then convert to a number when calculating payback.Calculate Lifetime Value from Subscription Revenue
Once a customer is tagged with acquisition data, track what they've spent. Query Stripe's subscriptions and invoices to calculate total revenue.
Retrieve subscription history and sum revenue
Use subscriptions.list() filtered by customer to pull all subscriptions. Then fetch invoices.list() to get the actual amount paid over time.
async function getCustomerLifetimeValue(customerId) {
const invoices = await stripe.invoices.list({
customer: customerId,
status: 'paid', // Only count invoices that were actually paid
limit: 100
});
let totalRevenue = 0;
for (const invoice of invoices.data) {
totalRevenue += invoice.amount_paid / 100; // Convert from cents to dollars
}
return totalRevenue;
}
const ltv = await getCustomerLifetimeValue('cus_ABC123');
console.log(`Customer LTV: $${ltv}`);Calculate months until CAC payback
Compare acquisition cost to cumulative revenue month-by-month. Find the invoice date when cumulative revenue first exceeded the acquisition cost.
async function getCAcPaybackMonths(customerId) {
const customer = await stripe.customers.retrieve(customerId);
const acquisitionCost = parseFloat(customer.metadata.acquisition_cost);
const acquisitionDate = new Date(customer.metadata.acquisition_date);
const invoices = await stripe.invoices.list({
customer: customerId,
status: 'paid',
limit: 100
});
let cumulativeRevenue = 0;
let paybackDate = null;
for (const inv of invoices.data.sort((a, b) => a.created - b.created)) {
cumulativeRevenue += inv.amount_paid / 100;
if (cumulativeRevenue >= acquisitionCost && !paybackDate) {
paybackDate = new Date(inv.created * 1000);
break;
}
}
if (paybackDate) {
const months = Math.round((paybackDate - acquisitionDate) / (1000 * 60 * 60 * 24 * 30));
return months;
}
return null; // Not yet paid back
}amount_paid can include refunds applied after the fact. Always filter by status: 'paid' to avoid counting reversed invoices.Build Cohort Analysis by Acquisition Source
Individual payback periods are useful, but patterns emerge when you group by campaign. Use Stripe's search API to query customers by metadata.
Query customers by acquisition source and calculate aggregate payback
Use customers.search() to filter by metadata, then calculate average payback time for each campaign. This shows you which channels deliver the best ROI.
async function getPaybackByCohort(acquisitionSource) {
const customers = await stripe.customers.search({
query: `metadata['acquisition_source']:'${acquisitionSource}'`,
limit: 100
});
let paybackMonths = [];
for (const customer of customers.data) {
const months = await getCAcPaybackMonths(customer.id);
if (months !== null) paybackMonths.push(months);
}
const avgPayback = paybackMonths.length ?
(paybackMonths.reduce((a, b) => a + b) / paybackMonths.length).toFixed(1) :
'N/A';
return {
source: acquisitionSource,
totalCustomers: customers.data.length,
paidBackCustomers: paybackMonths.length,
avgPaybackMonths: avgPayback
};
}
const cohort = await getPaybackByCohort('google_ads');
console.log(cohort); // {source: 'google_ads', totalCustomers: 42, paidBackCustomers: 38, avgPaybackMonths: '2.3'}'Common Pitfalls
- Treating metadata as mutable—acquisition cost and date are set at customer creation and can't be changed. Plan for immutability upfront.
- Counting refunded invoices toward LTV—always filter by
status: 'paid'and checkamount_paidinstead ofamount_charged. - Not accounting for churn—a customer who canceled month 3 has different payback math than one still active. Track churn separately.
- Mixing test and live mode data—add
livemode: trueto your search query to exclude test customers from cohort analysis.
Wrapping Up
CAC payback requires connecting acquisition costs, subscription data, and timelines. By tagging customers at signup and calculating payback from invoice history, you can see exactly which campaigns deliver value. If you want to track this automatically across tools, Product Analyst can help.