Back to Blog

Stripe Usage-Based Billing 2026: Meters & Meter Events Guide

Implement SaaS usage-based billing with Stripe: metered subscriptions, usage record aggregation, overage pricing tiers, real-time meter reporting, and billing period reconciliation.

Viprasol Tech Team
14 min read
Updated 2027

SaaS Usage-Based Billing in 2026: Metered Stripe Subscriptions, Aggregation, and Overage Charges

Quick answer. Usage-based Stripe billing needs three layers: track usage in your own database, report it to Stripe via the Billing Meters API (the 2024+ recommended approach, with Usage Records for legacy), and handle overages, thresholds, and credits. It supports pay-per-use, tiered, and hybrid base-plus-usage pricing.

Usage-based billing (UBB) charges customers for what they actually use — API calls, seats, GB of storage, messages sent. It's the fastest-growing pricing model in B2B SaaS because it aligns cost with value: customers start small and expand naturally without friction.

Implementing it correctly requires three layers: tracking usage in your own database, reporting usage records to Stripe at billing period end (or real-time), and handling overages, thresholds, and credits. This post covers the full stack using Stripe's Billing Meters API (the 2024+ recommended approach) and the older Usage Records API for compatibility.


Pricing Models Supported

ModelDescriptionStripe feature
Pay-per-useEvery unit billed (no base fee)Metered price
Included + overageN units included, charge above NGraduated tiers
Prepaid creditsBuy credits, consume themCredits + meters
HybridFlat seat fee + metered API callsMultiple line items

Database: Track Usage Locally First

Always record usage in your own database before reporting to Stripe. This gives you:

  • Audit trail independent of Stripe
  • Ability to show real-time usage to customers
  • Data for billing period reconciliation
CREATE TABLE usage_events (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  workspace_id    UUID NOT NULL REFERENCES workspaces(id),
  metric          TEXT NOT NULL,        -- 'api_calls', 'storage_gb', 'messages'
  quantity        NUMERIC NOT NULL,
  idempotency_key TEXT NOT NULL UNIQUE, -- Prevent double-counting
  occurred_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
  reported_at     TIMESTAMPTZ,          -- NULL until reported to Stripe
  stripe_event_id TEXT                  -- Stripe's meter event ID
);

CREATE INDEX idx_usage_events_workspace_metric
  ON usage_events(workspace_id, metric, occurred_at DESC);

CREATE INDEX idx_usage_events_unreported
  ON usage_events(reported_at)
  WHERE reported_at IS NULL;

-- Aggregated view for billing period
CREATE VIEW current_period_usage AS
SELECT
  workspace_id,
  metric,
  SUM(quantity) AS total_quantity,
  COUNT(*) AS event_count,
  MIN(occurred_at) AS first_event,
  MAX(occurred_at) AS last_event
FROM usage_events
WHERE occurred_at >= date_trunc('month', now())
GROUP BY workspace_id, metric;

🚀 SaaS MVP in 8 Weeks — Seriously

We have launched 50+ SaaS platforms. Multi-tenant architecture, Stripe billing, auth, role-based access, and cloud deployment — all handled by one senior team.

  • Week 1–2: Architecture design + wireframes
  • Week 3–6: Core features built + tested
  • Week 7–8: Launch-ready on AWS/Vercel with CI/CD
  • Post-launch: Maintenance plans from month 3

Recording Usage in Application Code

// lib/billing/usage-tracker.ts
import { db } from "@/lib/db";
import { createHash } from "crypto";

export async function recordUsage({
  workspaceId,
  metric,
  quantity,
  idempotencyKey,
}: {
  workspaceId: string;
  metric: string;
  quantity: number;
  idempotencyKey?: string;
}) {
  // Deterministic idempotency key if not provided
  const key = idempotencyKey ?? createHash("sha256")
    .update(`${workspaceId}:${metric}:${Date.now()}:${Math.random()}`)
    .digest("hex");

  await db.usageEvent.upsert({
    where: { idempotencyKey: key },
    create: {
      workspaceId,
      metric,
      quantity,
      idempotencyKey: key,
    },
    update: {}, // Already recorded — idempotent
  });
}

// Usage in API middleware
export async function apiMiddleware(req: Request, workspaceId: string) {
  // Process request...
  
  // Record usage after successful response
  await recordUsage({
    workspaceId,
    metric: "api_calls",
    quantity: 1,
    idempotencyKey: `api:${req.headers.get("x-request-id")}`,
  });
}

Stripe Billing Meters API (2024+ recommended)

Stripe's new Meters API replaces Usage Records for new integrations — it supports real-time reporting, deduplication, and better event model:

// lib/billing/stripe-meters.ts
import Stripe from "stripe";
import { db } from "@/lib/db";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-06-20",
});

// Create a meter in Stripe (one-time setup, do in stripe dashboard or script)
export async function setupMeters() {
  const apiCallsMeter = await stripe.billing.meters.create({
    display_name: "API Calls",
    event_name: "api_calls",
    default_aggregation: {
      formula: "sum",  // 'sum' | 'count' | 'last'
    },
    customer_mapping: {
      event_payload_key: "stripe_customer_id",
      type: "by_id",
    },
  });

  console.log("Meter ID:", apiCallsMeter.id);
  // Store this ID in your config/env
}

// Report a usage event to Stripe in real-time
export async function reportMeterEvent({
  stripeCustomerId,
  metricName,
  quantity,
  idempotencyKey,
  occurredAt,
}: {
  stripeCustomerId: string;
  metricName: string;
  quantity: number;
  idempotencyKey: string;
  occurredAt: Date;
}) {
  const event = await stripe.billing.meterEvents.create(
    {
      event_name: metricName,
      payload: {
        stripe_customer_id: stripeCustomerId,
        value: String(quantity),
      },
      timestamp: Math.floor(occurredAt.getTime() / 1000),
    },
    {
      idempotencyKey, // Stripe deduplicates on this key
    }
  );

  return event;
}

SaaS - Stripe Usage-Based Billing 2026: Meters & Meter Events Guide

💡 The Difference Between a SaaS Demo and a SaaS Business

Anyone can build a demo. We build SaaS products that handle real load, real users, and real payments — with architecture that does not need to be rewritten at 1,000 users.

  • Multi-tenant PostgreSQL with row-level security
  • Stripe subscriptions, usage billing, annual plans
  • SOC2-ready infrastructure from day one
  • We own zero equity — you own everything

Batch Reporting Worker (for async approach)

Real-time reporting adds latency to each API call. A background worker reports usage in batches every few minutes:

// workers/usage-reporter.ts
import { db } from "@/lib/db";
import { stripe } from "@/lib/stripe";
import { chunk } from "lodash-es";

const BATCH_SIZE = 100;
const REPORT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes

export async function reportUnsentUsage() {
  // Claim a batch of unreported events (advisory lock prevents double-report)
  const events = await db.$queryRaw<{
    id: string;
    workspaceId: string;
    metric: string;
    quantity: number;
    idempotencyKey: string;
    occurredAt: Date;
    stripeCustomerId: string;
  }[]>`
    SELECT u.id, u.workspace_id, u.metric, u.quantity,
           u.idempotency_key, u.occurred_at, s.stripe_customer_id
    FROM usage_events u
    JOIN workspace_subscriptions s ON s.workspace_id = u.workspace_id
    WHERE u.reported_at IS NULL
      AND s.stripe_customer_id IS NOT NULL
    ORDER BY u.occurred_at ASC
    LIMIT ${BATCH_SIZE}
    FOR UPDATE SKIP LOCKED
  `;

  if (events.length === 0) return;

  const results = await Promise.allSettled(
    events.map((event) =>
      stripe.billing.meterEvents.create(
        {
          event_name: event.metric,
          payload: {
            stripe_customer_id: event.stripeCustomerId,
            value: String(event.quantity),
          },
          timestamp: Math.floor(event.occurredAt.getTime() / 1000),
        },
        { idempotencyKey: event.idempotencyKey }
      ).then((stripeEvent) => ({ event, stripeEventId: stripeEvent.id }))
    )
  );

  // Mark successful reports
  const successIds: string[] = [];
  const stripeEventMap: Record<string, string> = {};

  for (const result of results) {
    if (result.status === "fulfilled") {
      successIds.push(result.value.event.id);
      stripeEventMap[result.value.event.id] = result.value.stripeEventId;
    } else {
      console.error("Failed to report usage event:", result.reason);
    }
  }

  if (successIds.length > 0) {
    await db.$executeRaw`
      UPDATE usage_events
      SET
        reported_at = now(),
        stripe_event_id = CASE id
          ${successIds.map((id) => `WHEN '${id}' THEN '${stripeEventMap[id]}'`).join(" ")}
        END
      WHERE id = ANY(${successIds}::uuid[])
    `;
  }

  console.log(`Reported ${successIds.length}/${events.length} usage events`);
}

Stripe Product and Price Setup

// scripts/setup-stripe-products.ts
// One-time setup: creates products and prices in Stripe

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-06-20" });

async function setup() {
  // Flat monthly seat fee
  const seatProduct = await stripe.products.create({
    name: "Pro Plan — Per Seat",
    metadata: { type: "seat" },
  });

  const seatPrice = await stripe.prices.create({
    product: seatProduct.id,
    unit_amount: 1500,        // $15/seat
    currency: "usd",
    recurring: { interval: "month" },
  });

  // Metered API calls (graduated tiers)
  const apiProduct = await stripe.products.create({
    name: "API Calls",
    metadata: { type: "metered" },
  });

  const apiPrice = await stripe.prices.create({
    product: apiProduct.id,
    currency: "usd",
    recurring: {
      interval: "month",
      usage_type: "metered",
      aggregate_usage: "sum",
    },
    billing_scheme: "tiered",
    tiers_mode: "graduated",
    tiers: [
      { up_to: 10_000, unit_amount: 0 },      // First 10K free
      { up_to: 100_000, unit_amount: 5 },      // 10K–100K: $0.005 each
      { up_to: 1_000_000, unit_amount: 2 },    // 100K–1M: $0.002 each
      { up_to: "inf", unit_amount: 1 },        // >1M: $0.001 each
    ],
    // Link to Stripe meter
    currency_options: {},
  });

  console.log("Seat price ID:", seatPrice.id);
  console.log("API price ID:", apiPrice.id);
}

Subscription with Multiple Price Items

// lib/billing/create-subscription.ts

export async function createSubscription({
  stripeCustomerId,
  seatCount,
  seatPriceId,
  apiCallsPriceId,
}: {
  stripeCustomerId: string;
  seatCount: number;
  seatPriceId: string;
  apiCallsPriceId: string;
}) {
  const subscription = await stripe.subscriptions.create({
    customer: stripeCustomerId,
    items: [
      {
        price: seatPriceId,
        quantity: seatCount,    // Flat: $15 × seatCount/month
      },
      {
        price: apiCallsPriceId, // Metered: no quantity here; reported via meter events
      },
    ],
    billing_cycle_anchor: "now",
    proration_behavior: "create_prorations",
    payment_behavior: "default_incomplete",
    expand: ["latest_invoice.payment_intent"],
  });

  return subscription;
}

Real-Time Usage Display to Customers

// app/api/usage/current/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getWorkspaceContext } from "@/lib/auth/workspace-context";
import { db } from "@/lib/db";
import { stripe } from "@/lib/stripe";
import { PLANS } from "@/lib/billing/plans";

export async function GET(req: NextRequest) {
  const ctx = await getWorkspaceContext();
  if (!ctx) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const [localUsage, subscription] = await Promise.all([
    // Local usage (real-time, includes unreported events)
    db.usageEvent.groupBy({
      by: ["metric"],
      where: {
        workspaceId: ctx.workspaceId,
        occurredAt: { gte: startOfCurrentMonth() },
      },
      _sum: { quantity: true },
    }),

    // Stripe subscription for limits
    db.workspaceSubscription.findFirst({
      where: { workspaceId: ctx.workspaceId },
      select: { plan: true, stripeSubscriptionId: true },
    }),
  ]);

  const plan = PLANS[subscription?.plan ?? "starter"];
  const usageMap = Object.fromEntries(
    localUsage.map((u) => [u.metric, u._sum.quantity ?? 0])
  );

  return NextResponse.json({
    period: {
      start: startOfCurrentMonth(),
      end: endOfCurrentMonth(),
    },
    metrics: {
      api_calls: {
        used: usageMap["api_calls"] ?? 0,
        included: plan.limits.apiCallsPerMonth,
        overage: Math.max(0, (usageMap["api_calls"] ?? 0) - plan.limits.apiCallsPerMonth),
      },
    },
  });
}

function startOfCurrentMonth() {
  const d = new Date();
  d.setDate(1); d.setHours(0, 0, 0, 0);
  return d;
}
function endOfCurrentMonth() {
  const d = new Date(startOfCurrentMonth());
  d.setMonth(d.getMonth() + 1);
  return d;
}

Usage Threshold Alerts

// lib/billing/usage-alerts.ts
const ALERT_THRESHOLDS = [0.8, 0.95, 1.0]; // 80%, 95%, 100%

export async function checkUsageThresholds(workspaceId: string) {
  const { metrics } = await getCurrentUsage(workspaceId);

  for (const [metric, data] of Object.entries(metrics)) {
    if (data.included === -1) continue; // Unlimited

    const ratio = data.used / data.included;

    for (const threshold of ALERT_THRESHOLDS) {
      if (ratio >= threshold) {
        const alertKey = `${workspaceId}:${metric}:${threshold}:${currentMonth()}`;

        // Only send each alert once per month
        const alreadySent = await db.usageAlert.findUnique({
          where: { alertKey },
        });

        if (!alreadySent) {
          await db.usageAlert.create({ data: { alertKey } });
          await sendUsageAlert({ workspaceId, metric, threshold, ratio });
        }
      }
    }
  }
}

Cost and Timeline

ComponentTimelineCost (USD)
Local usage tracking schema0.5 day$300–$500
Stripe meter setup + price config0.5–1 day$400–$800
Batch usage reporter worker1–2 days$800–$1,600
Customer usage dashboard1–2 days$800–$1,600
Usage threshold alerts0.5 day$300–$500
Full usage-based billing system2–3 weeks$12,000–$20,000

More on This Topic


What We Bring to the Table

We build usage-based billing systems for SaaS products — from simple API call metering through complex hybrid seat + usage pricing with credits, thresholds, and real-time dashboards. Our fintech team has shipped billing systems processing millions of usage events per day.

What we deliver:

  • Local usage event recording with idempotency keys
  • Stripe Billing Meters integration with real-time or batch reporting
  • Graduated tier pricing configuration
  • Customer-facing real-time usage dashboard
  • Threshold alerts at 80%, 95%, and 100% of limits

Explore our SaaS development services or contact us to implement usage-based billing.

How the Stripe Usage-Based Billing Metered Billing Docs Map to a Real Implementation

If you are working through the stripe usage-based billing metered billing docs and trying to turn them into shipped code, the key shift in 2026 is the move from legacy usage records to the Meters and Meter Events API. You define a meter once, then send meter events from your application each time a customer consumes a unit. Stripe aggregates those events server-side, so your billing model stays accurate even under high event volume.

In practice you attach the meter to a metered price, choose an aggregation method such as sum or last value, and let Stripe handle proration and invoicing. We have built this end to end for SaaS clients, and our senior engineers own the full integration, including idempotent event reporting and reconciliation, so your usage data and invoices never drift apart.

SaaSTypeScriptStripeBillingPostgreSQLFintech
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Building a SaaS Product?

We've helped launch 50+ SaaS platforms. Let's build yours — fast.

Free consultation • No commitment • Response within 24 hours

Viprasol · AI Agent Systems

Add AI automation to your SaaS product?

Viprasol builds custom AI agent crews that plug into any SaaS workflow — automating repetitive tasks, qualifying leads, and responding across every channel your customers use.