In short: AI margin per customer is what the customer paid you through Stripe, minus tax and Stripe's fee, minus what their AI usage cost. Stripe knows the first half, your AI provider knows the second, and nothing joins them unless both use the same customer ID.
A flat $20 plan can hide a customer who costs you $60 a month in tokens. Averages won't show it; per-customer numbers will. This guide shows how to build that join, and the traps that make the numbers wrong.
Your backend knows your user ID when it calls the AI provider. Stripe knows a cus_… customer ID. Pick one and use it on both sides. The simplest way is to write your user ID into Stripe metadata when you create the checkout session or subscription:
await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: "price_pro", quantity: 1 }],
subscription_data: { metadata: { user_id: user.id } },
customer_email: user.email,
// …
});Now every invoice for that subscription carries user_id in its subscription details, and you can attribute it without a lookup table. If you can't change checkout, store the cus_… ID on your user record and report AI usage under that instead.
| Stripe event | What it means for margin |
|---|---|
invoice.paid | Money in. billing_reason tells a first payment from a renewal |
customer.subscription.created (trialing) | A trial starts: AI cost begins, revenue is $0 |
customer.subscription.updated | Auto-renew switched off when cancel_at_period_end turns true |
customer.subscription.deleted | Access ended: no more revenue, cost may continue |
charge.refunded | Money back out. amount_refunded is a running total, so count only the new part |
Verify the Stripe-Signature header on every request, and de-duplicate on the event ID: Stripe retries deliveries, sometimes hours later.
net = amount_paid − tax − (amount_paid × 2.9% + $0.30) // Stripe's standard US card feeOn a $20 invoice that is about $19.12 before tax, and less if you collect sales tax or VAT. The exact fee is on the balance transaction; the standard rate is close enough for margin unless you have negotiated pricing or many international cards.
After each AI call, read the token counts from the response and price them for the model used. Cached input, cache writes and long prompts are priced differently, so use the provider's full price table. Store cost per user per day; that is all the join needs. The OpenAI and Anthropic guides cover the details.
A webhook only sees events from the day you add it. Customers who paid last month look like free users until they renew. Backfill once through the API with a restricted key that can only read:
/v1/invoices?status=paid, /v1/refunds and /v1/subscriptions for the period you care about, and run each object through the same mapping as the webhook.FiscaLLM does all of the above. Paste one webhook URL from the Setup page, then paste a read-only restricted key to import the last 12 months; the key is used once and never stored, and invoices are counted once whether they arrive by webhook or import. Report AI usage with one line per call, and you get margin per customer, per plan and per trial cohort, with alerts when a customer starts losing you money.
meter.trackOpenAI(res, { userId, feature: "chat" }); // userId = metadata.user_id on the subscription
// Setup → Connect Stripe (webhook) → Import the last 12 months (restricted key)It's free and open source (MIT), and you can self-host it so your revenue data never leaves your servers. The Stripe connector is in beta and USD only. Open the demo dashboard (sample data, no sign-up), read the Stripe docs, or start free. Not on Stripe? See margin from a revenue CSV or RevenueCat.
No. Stripe records what customers pay and, with usage-based billing, what you report to it, but it doesn't know what each customer cost you at OpenAI or Anthropic. That has to come from your backend and be joined by customer ID.
Usage billing passes cost on to the customer; it doesn't tell you your margin. Most AI apps keep a flat plan and need to see which customers the flat price doesn't cover.
A restricted key with only Read access can't move money or change anything. Give it the fewest permissions the job needs, and delete it once the import is done.