The moment you decide to monetize a product built on Claude API, billing design becomes the first real obstacle. Flat-rate pricing sounds simple, but API costs don't stay flat — a single user who sends lengthy document analysis requests can cost you more than your entire monthly plan revenue.
I've built Claude-powered services with Stripe metered billing, and the biggest lesson was: don't try to build the perfect billing system on day one. Ship a working minimal implementation, see how real users behave, then optimize. This guide gives you that minimal implementation — a complete Node.js setup that records Claude token consumption to Stripe Meter Events, with the production gotchas I learned the hard way.
Flat Rate vs. Usage-Based — The Real Trade-offs
Flat-rate pricing has one undeniable advantage: it's easy to explain. "$9.99/month" converts well on landing pages.
The problem with flat-rate for Claude API products is variance. A user asking short questions might consume 200 tokens per request. A user uploading 20-page PDFs for summarization might consume 8,000 tokens per request — 40x more. If you designed your $9.99 plan assuming the former and attract the latter, you lose money on every active user.
Usage-based billing solves this structurally. You charge proportional to consumption, so your margin holds regardless of usage patterns.
There's another benefit I didn't anticipate: you accumulate real usage data before committing to a pricing model. Launch with metered billing, watch how your actual users behave for a month, then design an informed flat-rate or tiered plan based on real percentiles. You can't do that if you start with flat-rate.
For background on Claude API pricing and how to think about cost structure, see Claude API Pricing Guide 2026.
How Stripe Meter Events Work
Stripe's recommended approach for usage-based billing is now Meter Events — the previous Usage Records API has been deprecated for new implementations.
The model has three parts:
- Meter: Defines what you're measuring (e.g., "total tokens consumed"), including how values aggregate over a billing period
- Meter Event: An API call you make to record actual usage as it happens
- Subscription Price: A price object linked to the Meter, which Stripe uses to calculate the amount to charge at period end
Every time a user calls your Claude-powered feature, you record the token count to Stripe. At the end of the billing period, Stripe sums everything up and charges the customer's card automatically.
Getting Token Count from Claude API
Claude's response object includes token usage data out of the box. No extra API calls needed.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello" }],
});
// Token usage is always present in the response
const inputTokens = response.usage.input_tokens;
const outputTokens = response.usage.output_tokens;
const totalTokens = inputTokens + outputTokens;
console.log(
`Tokens: ${inputTokens} input + ${outputTokens} output = ${totalTokens} total`
);
// Example: Tokens: 12 input + 38 output = 50 totalIf you're using prompt caching, cache_read_input_tokens will also appear in response.usage. For a minimal implementation, tracking input_tokens + output_tokens is sufficient. See Claude API Token Counting and Cost Optimization for more advanced tracking.
The Minimal Implementation — Complete Node.js Code
Here's the full working implementation. One function handles both the Claude API call and the Stripe billing event.
// claude-billing.js
import Anthropic from "@anthropic-ai/sdk";
import Stripe from "stripe";
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Must match the event_name you set when creating the Meter in Stripe
const METER_EVENT_NAME = "claude_tokens";
/**
* Call Claude API and record token consumption to Stripe.
* @param {string} stripeCustomerId - The customer's Stripe ID (e.g., "cus_XXXXXXXXXX")
* @param {string} userMessage - The user's message to send to Claude
*/
async function callClaudeWithBilling(stripeCustomerId, userMessage) {
// 1. Call the Claude API
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: userMessage }],
});
const totalTokens =
response.usage.input_tokens + response.usage.output_tokens;
// 2. Record usage to Stripe Meter Events
try {
await stripe.v2.billing.meterEvents.create({
event_name: METER_EVENT_NAME,
payload: {
value: String(totalTokens), // Stripe requires a string value
stripe_customer_id: stripeCustomerId,
},
});
console.log(
`✅ Billing recorded: ${totalTokens} tokens for customer ${stripeCustomerId}`
);
} catch (billingError) {
// Always log billing failures — silent failures mean lost revenue
console.error(`⚠️ Billing record failed: ${billingError.message}`, {
customerId: stripeCustomerId,
tokens: totalTokens,
timestamp: new Date().toISOString(),
});
// TODO: Add to retry queue
}
return {
content: response.content[0].text,
tokensUsed: totalTokens,
};
}
// Example usage
const result = await callClaudeWithBilling(
"cus_XXXXXXXXXX", // Replace with actual Stripe customer ID
"Explain Next.js performance optimization strategies"
);
console.log("Response:", result.content);
console.log("Tokens used:", result.tokensUsed);Expected behavior: Claude responds to the user message, and a claude_tokens event appears in your Stripe dashboard. At month end, Stripe automatically sums all events and generates an invoice.
Setting Up Stripe (One-Time Configuration)
Before the code can run, configure three things in Stripe.
Step 1: Create a Meter
Stripe Dashboard → Billing → Meters → "+ Create meter"
- Event name:
claude_tokens(must matchMETER_EVENT_NAMEin your code) - Default aggregation:
sum(total tokens consumed per billing period)
After creation, save the Meter ID — you'll need it when creating the price.
Step 2: Create a Usage-Based Price
Products → "+ Add product" → Select "Usage-based" as the pricing model
- Meter: select your
claude_tokensmeter - Per unit: set your price per 1,000 tokens
A reasonable starting point: claude-sonnet-4-6 costs $3/1M input tokens and $15/1M output tokens. Set your per-unit price to cover your cost plus margin, then adjust once you see actual usage data.
Step 3: Create Subscriptions on User Signup
// Call this when a new user registers
const subscription = await stripe.subscriptions.create({
customer: stripeCustomerId,
items: [{ price: process.env.STRIPE_USAGE_PRICE_ID }],
});With these three steps configured, the billing function above will correctly record and attribute usage.
Three Production Gotchas
These are the issues I ran into after going live — not obvious from the documentation.
Gotcha 1: Meter Event failures silently drop revenue
The Claude API call succeeds, but the Stripe event send fails due to a network hiccup. The user gets their response, you incur the API cost, but nothing gets billed. This is the most dangerous failure mode because there's no immediate symptom.
The try/catch in the code above at least logs the failure. In production, you want more than a log — failed events should go into a database or queue for retry. A simple approach: save { customerId, tokens, timestamp, retried: false } to a database table on failure, and have a scheduled job retry anything with retried: false.
Gotcha 2: stripe.v2 requires a recent SDK version
Meter Events live under stripe.v2.billing.meterEvents.create() — not stripe.billing or stripe.usageRecords. Older versions of the Stripe SDK don't have the v2 namespace at all and will throw a confusing error. Run npm install stripe@latest before starting, and verify your Stripe SDK version is ≥ 12.0.
Gotcha 3: Design customer ID storage before you ship
The stripeCustomerId parameter works cleanly in this example, but in a real app you need a way to fetch it for the current user. The standard pattern: add a stripe_customer_id column to your users table, populate it during signup, and read it from the session when handling API requests. Retrofitting this after launch means migrating existing billing records — better to design it upfront.
Finishing Touches Before You Ship — Caps and Webhooks
Once the minimal version works, two things are worth adding before production: a cap that prevents overages, and a webhook that catches the close of each billing cycle.
Usage-based pricing leaves users with one nagging worry — they can't predict the bill. A monthly cap softens that and lowers the barrier to adoption. When I added metered billing to a personal project — one of the indie apps I build and run myself under Dolice — a handful of users burned through several times my expected volume in the first few days. Bolting a cap on afterward meant rewiring the request path, so I'd build the limit check at the same time you wire up billing.
// Middleware that records monthly usage in your DB and checks the cap
export async function checkAndTrackUsage(
userId: string,
estimatedCredits: number
): Promise<{ allowed: boolean; currentUsage: number; limit: number }> {
const plan = await getUserPlan(userId);
const currentUsage = await getMonthlyUsage(userId);
const limit = plan.monthlyCredits; // e.g. 1000 credits on the Lite plan
if (currentUsage + estimatedCredits > limit) {
return { allowed: false, currentUsage, limit };
}
await incrementMonthlyUsage(userId, estimatedCredits);
return { allowed: true, currentUsage: currentUsage + estimatedCredits, limit };
}
// Using it in an API endpoint
export async function POST(request: Request) {
const { prompt, userId } = await request.json();
const usageCheck = await checkAndTrackUsage(userId, 10); // ~10 credits estimated
if (!usageCheck.allowed) {
return Response.json(
{
error: "Monthly usage limit reached. Please upgrade your plan.",
currentUsage: usageCheck.currentUsage,
limit: usageCheck.limit,
},
{ status: 429 }
);
}
const subscriptionItemId = await getSubscriptionItemId(userId);
const result = await callClaudeWithMetering(prompt, userId, subscriptionItemId);
return Response.json({ result });
}When the cap is hit, return a 429 and point users toward an upgrade. Including the current usage and limit in the error body helps them see exactly where they stand.
At the close of each billing cycle, listen for Stripe webhooks to reset the monthly counter. Handle two events and the basics are covered: reset the counter to zero on invoice.payment_succeeded, and suspend the service on invoice.payment_failed.
// app/api/stripe-webhook/route.ts
export async function POST(request: Request) {
const sig = request.headers.get("stripe-signature")!;
const body = await request.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch {
return new Response("Webhook signature verification failed", { status: 400 });
}
switch (event.type) {
case "invoice.payment_succeeded": {
const invoice = event.data.object as Stripe.Invoice;
await resetMonthlyUsage(invoice.customer as string); // reset the monthly counter
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
await suspendUserService(invoice.customer as string); // pause on payment failure
break;
}
}
return new Response("ok");
}Skipping signature verification (constructEvent) would let anyone POST fake events to your endpoint. Always verify with STRIPE_WEBHOOK_SECRET.
One last note on timing. If you use Claude's streaming API, token counts aren't final until the whole response completes. Reporting mid-stream means under-reporting whenever a connection drops. The safe approach is to report once, after the response finishes. If you want a live usage display, count locally in your database for the UI and keep the Stripe report asynchronous and post-completion.
Test with Stripe's Test Mode First
Stripe test mode uses sk_test_... keys and generates no real charges. Set your environment variables to test credentials and send a few real requests to verify the integration.
In your Stripe Dashboard, navigate to Billing → Usage → your Meter. You should see events appearing with the correct token counts and customer IDs. Once this looks right, switching to production is just swapping sk_test_... for sk_live_....
For a more complete implementation that adds retry logic, usage caps, and plan tier switching, see Monetizing a Personal SaaS with Claude API.
The best billing system is the one that ships. This minimal version handles the core loop correctly — Claude usage gets measured and recorded to Stripe. Ship it, watch how your first users actually behave, and then build the sophistication your real usage patterns actually require.
Required Environment Variables
Before running the code, you'll need four environment variables:
# .env
ANTHROPIC_API_KEY=sk-ant-... # Your Anthropic API key
STRIPE_SECRET_KEY=sk_test_... # Stripe secret key (use sk_live_ for production)
STRIPE_USAGE_PRICE_ID=price_... # Price ID from Step 2 of Stripe setup
STRIPE_METER_ID=mtr_... # Optional: Meter ID for referenceKeep STRIPE_SECRET_KEY server-side only — never expose it in client-side code or commit it to version control. Use a secrets manager or environment variable injection in your deployment pipeline.