How to meter LLM token usage per customer for billing
If you resell model access inside your product, every customer's bill depends on how many tokens they used. This guide shows how to capture those numbers from the provider response, tie them to the right customer, and turn them into a monthly quantity you can invoice.
1. Read token counts from the provider, don't estimate them
Both major APIs return exact token counts with every response. Use those instead of counting tokens yourself:
| Provider | Field in the response | Source |
|---|---|---|
| OpenAI Chat Completions | usage.prompt_tokens, usage.completion_tokens, usage.total_tokens | OpenAI API reference |
| Anthropic Messages | usage.input_tokens, usage.output_tokens | Anthropic Messages API |
With streaming, the usage arrives at the end of the stream (OpenAI needs stream_options: {"include_usage": true}; Anthropic reports it in the message start and delta events). Record usage once the stream finishes, not per chunk.
2. Decide what you bill: input, output or both
Providers price input and output tokens differently, and output usually costs more. You have two honest options:
- One meter for total tokens: simplest for customers; pick a blended price.
- Two meters,
input_tokensandoutput_tokens: mirrors your own cost and protects margin on long outputs.
3. Attribute every call to a customer
The provider knows your API key, not your customer. Attach your customer or subscription id at the point you make the call, typically from the authenticated request in your own backend. If you bill through Stripe, use the Stripe customer id so the monthly quantity maps straight onto an invoice.
4. Make the usage event retry-safe
If your usage report times out and you resend it, the customer must not be charged twice. Use a stable key per model call; the provider's response id (OpenAI id like chatcmpl-…, Anthropic id like msg_…) works well. More in deduping usage events with idempotency keys.
5. Send the event to Usage Lab
Create a sum meter once per token type:
curl -X POST https://usage-lab.marc-cf2.workers.dev/api/v1/meters \
-H "Authorization: Bearer $USAGE_LAB_KEY" -H "Content-Type: application/json" \
-d '{"key":"input_tokens","aggregation":"sum","name":"LLM input tokens"}'
curl -X POST https://usage-lab.marc-cf2.workers.dev/api/v1/meters \
-H "Authorization: Bearer $USAGE_LAB_KEY" -H "Content-Type: application/json" \
-d '{"key":"output_tokens","aggregation":"sum","name":"LLM output tokens"}'
After each model call, send both numbers in one batch. Events without their own key get <header>:0, <header>:1, so a retry of the whole batch is deduped:
curl -X POST https://usage-lab.marc-cf2.workers.dev/api/v1/usage \
-H "Authorization: Bearer $USAGE_LAB_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: chatcmpl-abc123" \
-d '[
{"meter":"input_tokens","subscription":"cus_123","value":812,"properties":{"model":"gpt-4o-mini"}},
{"meter":"output_tokens","subscription":"cus_123","value":240,"properties":{"model":"gpt-4o-mini"}}
]'
In Node, right after an OpenAI call:
const completion = await openai.chat.completions.create({ model, messages });
const { prompt_tokens, completion_tokens } = completion.usage;
await fetch("https://usage-lab.marc-cf2.workers.dev/api/v1/usage", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.USAGE_LAB_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": completion.id, // stable across retries
},
body: JSON.stringify([
{ meter: "input_tokens", subscription: customerId, value: prompt_tokens },
{ meter: "output_tokens", subscription: customerId, value: completion_tokens },
]),
});
With Anthropic, use message.id as the key and message.usage.input_tokens / output_tokens as the values. A resend with the same key and payload returns "status": "duplicate" and is not counted again; usage up to 30 days late lands in the month it happened.
6. Read each customer's monthly total
curl "https://usage-lab.marc-cf2.workers.dev/api/v1/meters/output_tokens/rollups?month=2026-10&subscription=cus_123" \
-H "Authorization: Bearer $USAGE_LAB_KEY"
The response has total_quantity and a rollups array with quantity, event_count and pending_export_quantity per subscription.
7. Bill it
Stripe prices metered usage through usage-based billing, fed by meter events. Usage Lab's automatic push of monthly totals to Stripe Billing Meters is coming soon and runs in dry-run today, so for now read the rollup and report it to Stripe or your own biller.
Common mistakes
- Estimating tokens with a tokenizer instead of using the provider's
usagefields. - Using a random UUID generated per attempt as the idempotency key, which makes every retry look new.
- Recording usage before the call succeeds, so failed calls get billed.
- Dropping streamed usage because it only arrives in the final event.