DevStacked
Jul 11, 202613 min read

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's value during the billing period. Meters support sum, count, and last — pick sum for most metered billing (API calls, tokens, storage GB), count when every event is worth exactly 1 unit, and last for snapshot-style metrics (like "current number of seats").
  • customer_mapping tells 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" with tiers_mode: "graduated" instead of per_unit, and set the first tier's up_to to 100 with unit_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.id in 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 listen running 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_amount per event (what we built above with per_unit)
  • Free tier + overage — first N units free, then a per-unit charge (the tiered example 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

Continue Learning

StripeUsage-Based BillingMetered BillingNext.jsSaaSTypeScript
Share On