How to Build Usage-Based Billing with Stripe (2026 Guide)
Flat monthly pricing works great until it doesn't. If you're building an AI tool, an API product, or anything where one customer might use 10x more than another, charging everyone the same $29/month either underprices your power users or overprices your casual ones. That's exactly the problem usage-based billing solves — customers pay for what they actually use.
The tricky part? Stripe changed how this works. If you've searched "Stripe usage-based billing" and found tutorials using usage_type: 'metered' with createUsageRecord(), that API is deprecated. Since Stripe API version 2025-03-31.basil, every metered price needs a backing Meter object instead.
In this guide, you'll learn how to build usage-based billing with Stripe's current Meters API in a Next.js 16 app — from creating your first meter to sending usage events and letting Stripe handle the invoicing automatically.
Why Usage-Based Billing Is Tricky
The confusing part isn't the concept — it's that Stripe has three different usage-based billing systems depending on when you read the docs:
- Legacy usage records (
subscriptionItems.createUsageRecord) — deprecated, don't use this anymore - Meters API — the current, stable, GA approach for straightforward metered billing (what this guide covers)
- Advanced usage-based billing (pricing plans) — a newer preview system for complex hybrid pricing with credits and multiple usage dimensions, aimed at larger AI/SaaS platforms
For most beginners adding "pay for what you use" pricing to a SaaS product, the Meters API is the right tool. It's simpler, it's stable, and it covers the vast majority of real use cases: API calls, AI tokens, storage, emails sent, whatever unit your product bills on.
💡 Tip: One genuinely useful change with Meters — you don't need a subscription to exist before recording usage. You can track usage during a free trial and attach the customer to a subscription later.
User
│
▼
Makes API Call
│
▼
Your Server
│
▼
stripe.billing.meterEvents.create()
│
▼
Stripe Meter
│
▼
Aggregates Usage
│
▼
Subscription
│
▼
End of Month
│
▼
Invoice
What You'll Need
This guide uses:
- Next.js 16 with the App Router
- TypeScript in strict mode
- Stripe Node SDK (latest version)
- A Stripe account in test mode
We'll build a simple example: an app that charges $0.01 per API call, with the first 100 calls free each month.
Step 1: Install Stripe
npm install stripe
Create a reusable Stripe client:
// src/lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2026-06-24.dahlia",
});
⚠️ Common Mistake: Never import this file into a Client Component. The secret key must stay server-side only — treat it the same way you'd treat a database password.
Add your keys to .env.local:
STRIPE_SECRET_KEY=sk_test_xxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxx
NEXT_PUBLIC_APP_URL=http://localhost:3000
Step 2: Understand the Four Building Blocks
Before writing code, it helps to know what each Stripe object actually does — this is where most beginners get lost.
- Meter — defines what you're tracking and how to add it up (sum, count, or last value). Think of it as a labeled counter.
- Price — attaches to a Meter and sets the cost per unit (e.g. $0.01 per event).
- Subscription — links a customer to that Price, so Stripe knows to bill them for it.
- Meter Event — the raw usage data you send to Stripe every time something billable happens (e.g. "customer X made 1 API call").
Here's how they connect:
Meter (tracks "api_calls")
↓
Price ($0.01 per unit, references the Meter)
↓
Subscription (customer subscribed to that Price)
↓
Meter Events (you send these as usage happens)
↓
Stripe aggregates → generates invoice at period end
Example SaaS Pricing
Suppose you're building an AI writing application.
- First 100 prompts free
- $0.02 per prompt afterwards
- Monthly billing cycle
Each prompt sends
await stripe.billing.meterEvents.create(...)
Stripe counts every prompt and invoices the customer automatically at the end of the month.
Step 3: Create a Stripe Meter
You only need to create a Meter once for each type of billable usage (for example, API calls, AI tokens, or storage), either in the Stripe Dashboard or via the API. Let's use the API so it's repeatable:
// scripts/create-meter.ts
import { stripe } from "@/lib/stripe";
async function createMeter() {
const meter = await stripe.billing.meters.create({
display_name: "API Calls",
event_name: "api_call", // you'll use this exact name when sending events
default_aggregation: {
formula: "sum", // add up every event's value during the billing period
},
customer_mapping: {
type: "by_id",
event_payload_key: "stripe_customer_id",
},
value_settings: {
event_payload_key: "value",
},
});
console.log("Meter created:", meter.id);
}
createMeter();
What's happening here:
event_name: "api_call"is the label you'll reference every time you record usage — it must match exactly.default_aggregation.formula: "sum"tells Stripe to add up every event'svalueduring the billing period. Meters supportsum,count, andlast— picksumfor most metered billing (API calls, tokens, storage GB),countwhen every event is worth exactly 1 unit, andlastfor snapshot-style metrics (like "current number of seats").customer_mappingtells Stripe which field in your event payload identifies the customer.
Run it once with npx tsx scripts/create-meter.ts and save the returned meter ID — you'll need it in the next step.
Step 4: Create a Stripe Metered Price
Now attach a Price to that Meter:
// scripts/create-price.ts
import { stripe } from "@/lib/stripe";
async function createPrice() {
const product = await stripe.products.create({
name: "API Usage",
});
const price = await stripe.prices.create({
currency: "usd",
product: product.id,
billing_scheme: "per_unit",
unit_amount_decimal: "1", // 1 cent per API call
recurring: {
interval: "month",
usage_type: "metered",
meter: "mtr_xxxxx", // the meter ID from Step 3
},
});
console.log("Price created:", price.id);
}
createPrice();
💡 Tip: Want the first 100 calls free every month? Use
billing_scheme: "tiered"withtiers_mode: "graduated"instead ofper_unit, and set the first tier'sup_toto100withunit_amount: 0.
const price = await stripe.prices.create({
currency: "usd",
product: product.id,
billing_scheme: "tiered",
tiers_mode: "graduated",
tiers: [
{ up_to: 100, unit_amount: 0 }, // first 100 calls free
{ up_to: "inf", unit_amount_decimal: "1" }, // then 1 cent each
],
recurring: {
interval: "month",
usage_type: "metered",
meter: "mtr_xxxxx",
},
});
Save this price ID as STRIPE_METERED_PRICE_ID in your .env.local.
Step 5: Create a Stripe Subscription for Metered Billing
When a user signs up, create a Stripe customer and subscribe them:
// src/actions/create-subscription.ts
"use server";
import { stripe } from "@/lib/stripe";
export async function createMeteredSubscription(email: string) {
const customer = await stripe.customers.create({ email });
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: process.env.STRIPE_METERED_PRICE_ID! }],
payment_behavior: "default_incomplete",
payment_settings: { save_default_payment_method: "on_subscription" },
});
// TODO: save customer.id and subscription.id against your user in your database
return { customerId: customer.id, subscriptionId: subscription.id };
}
What's happening here: unlike one-time payments, you don't specify a quantity upfront — the metered price has no fixed amount because you haven't reported usage yet. Stripe will calculate the invoice total at the end of the billing period based on the events you send.
⚠️ Common Mistake: Forgetting to store
customer.idin your own database. You'll need it every time you send a usage event, so save it right after creating the customer — don't rely on looking it up from Stripe later.
Step 6: Record Usage with Meter Events
This is the core of usage-based billing — every time your customer does something billable, send an event:
// src/lib/record-usage.ts
import { stripe } from "@/lib/stripe";
export async function recordApiCall(stripeCustomerId: string, quantity = 1) {
await stripe.billing.meterEvents.create({
event_name: "api_call",
payload: {
stripe_customer_id: stripeCustomerId,
value: quantity.toString(),
},
});
}
Then call it wherever the billable action happens — for example, inside an API route:
// src/app/api/generate/route.ts
import { NextResponse } from "next/server";
import { recordApiCall } from "@/lib/record-usage";
export async function POST(req: Request) {
const { stripeCustomerId } = await req.json();
// ... do the actual work (call an AI model, process a file, etc.) ...
// Report 1 unit of usage to Stripe
await recordApiCall(stripeCustomerId);
return NextResponse.json({ success: true });
}
What's happening here: event_name must match the meter's event_name from Step 3 exactly, or Stripe won't know which meter to attribute this to. Every event is timestamped automatically, and Stripe aggregates them in the background — you don't need to do any math yourself.
⚠️ Common Mistake: Sending a meter event before the customer's subscription exists doesn't fail — Stripe accepts it and attributes it to the customer. But if that customer never gets subscribed to a metered price, the usage just sits there unbilled. Always double-check your signup flow actually creates the subscription.
💡 Production Tip: Only report usage after the billable action succeeds. For example, if an AI request fails or an API call returns an error before completing, don't send a meter event. Otherwise customers could be billed for work that never happened.
Step 7: Handle Webhooks for Invoicing
Stripe automatically generates an invoice at the end of each billing period based on aggregated usage. You still need webhooks to know when that happens and keep your app in sync:
// src/app/api/webhook/route.ts
import { NextResponse } from "next/server";
import { headers } from "next/headers";
import Stripe from "stripe";
import { stripe } from "@/lib/stripe";
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const payload = await req.text();
const sig = (await headers()).get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json({ error: `Webhook Error: ${message}` }, { status: 400 });
}
switch (event.type) {
case "invoice.created": {
const invoice = event.data.object as Stripe.Invoice;
console.log("Usage invoice generated:", invoice.id, invoice.amount_due);
// TODO: notify the customer, or flag it for review before auto-finalizing
break;
}
case "invoice.paid": {
const invoice = event.data.object as Stripe.Invoice;
console.log("Usage invoice paid:", invoice.id);
// TODO: mark the billing period as settled in your database
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
console.log("Usage invoice payment failed:", invoice.id);
// TODO: notify the customer, restrict access if needed
break;
}
}
return NextResponse.json({ received: true }, { status: 200 });
}
Why req.text() instead of req.json()? Stripe's signature verification needs the raw, unparsed request body. Parsing it first breaks the signature check.
Step 8: Test Usage-Based Billing Locally
Use the Stripe CLI to forward webhooks and simulate the billing cycle:
stripe listen --forward-to localhost:3000/api/webhook
Send a test meter event directly with the CLI:
stripe billing meter_events create \
--event-name=api_call \
--payload stripe_customer_id=cus_xxxx \
--payload value=1
To see how the invoice looks without waiting a full month, use a test clock — it lets you fast-forward time in test mode so you can preview an end-of-period invoice in minutes instead of weeks.
💡 Tip: Always test with
stripe listenrunning in a separate terminal tab. If your webhook handler isn't receiving events, 90% of the time it's because this command isn't running.
Common Metered Billing Patterns
A few real-world pricing shapes you can build on top of what we've covered:
- Pure pay-as-you-go — no free tier,
unit_amountper event (what we built above withper_unit) - Free tier + overage — first N units free, then a per-unit charge (the
tieredexample above) - Hybrid subscription — a flat monthly base fee (a normal recurring Price) plus a metered Price as a second subscription item, so customers pay a base amount and usage on top
For hybrid pricing, just add both prices to the same subscription:
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [
{ price: "price_flat_base_fee" }, // e.g. $29/month base
{ price: "price_metered_overage" }, // usage-based on top
],
});
Frequently Asked Questions
Do I need a subscription before I can record usage?
No. Meter events are attributed to a customer as soon as you send them, even before a subscription exists. This is useful for tracking free-trial usage before a customer converts to paid.
Can I bill multiple products on separate meters?
Yes. Create a separate Meter and Price for each usage type (e.g. api_calls and storage_gb), then attach both prices to the customer's subscription as separate items.
What's the difference between sum, count, and last aggregation?
sum adds up every event's value — use this for things like tokens or API calls where each event carries a quantity. count treats every event as worth 1, regardless of payload. last only keeps the most recent value in the period — useful for snapshot metrics like "current active seats."
What happens if I accidentally send duplicate meter events?
Stripe bills exactly what you report — it won't automatically detect or filter out duplicate events for you. If your server retries a failed request and ends up sending the same usage event twice, that customer gets billed twice for the same action. To avoid this, use idempotency keys when calling meterEvents.create(), and make sure your application logic only records a usage event once per successful billable action, not once per attempt.
Does Stripe charge extra fees for usage-based billing?
Yes, Stripe Billing adds a small percentage fee on top of standard payment processing for metered subscriptions. Check Stripe's current pricing page before committing, since it affects your margins at scale.
Should I use the Meters API or the newer advanced usage-based billing (pricing plans)?
For most SaaS apps with one or two usage dimensions, the Meters API covered in this guide is simpler and fully stable. Advanced usage-based billing (pricing plans) is aimed at more complex cases — multiple pricing components, prepaid credits, and enterprise contracts — and is still rolling out as a newer offering.
Wrapping Up
You now have a working usage-based billing setup in Next.js 16 using Stripe's current Meters API — a Meter to track usage, a metered Price to set the cost, a subscription to link it to a customer, and meter events to report usage as it happens. Stripe handles the aggregation and invoicing automatically from there.
From here, natural next steps are adding usage dashboards for your customers (so they can see their consumption before the bill arrives), setting up usage alerts, and combining metered pricing with a flat base fee for a hybrid SaaS pricing model.
Helpful Resources
- Stripe Meters API Reference
- Stripe Usage-Based Billing Docs
- Stripe Meter Events API Reference
- Stripe Test Clocks Docs
- Stripe CLI Docs