Skip to main content

How to Add Usage-Based Billing to Stripe in Your Boilerplate (2026)

·StarterPick Team
stripebillingusage-basedguide2026

TL;DR

Usage-based billing is Stripe's metered billing feature. Instead of fixed monthly prices, customers pay for what they use (API calls, seats, storage, etc.). Implementation requires: (1) a metered price in Stripe, (2) tracking usage in your database, and (3) reporting usage to Stripe at billing period end. Total setup: 2-3 days.


Stripe Concepts

Before code, understand the Stripe model:

  • Metered price: A price with aggregate_usage: sum and billing_scheme: per_unit
  • Usage records: Events you report to Stripe via API (e.g., "user X made 150 API calls")
  • Billing period: Usage is reset to 0 at the start of each period
  • Invoice: Stripe generates an invoice at period end based on reported usage

Step 1: Create a Metered Price in Stripe

// Run once: create metered price in Stripe
const price = await stripe.prices.create({
  currency: 'usd',
  product: 'prod_XXXXX', // Your product ID
  recurring: {
    interval: 'month',
    usage_type: 'metered',
    aggregate_usage: 'sum',
  },
  billing_scheme: 'per_unit',
  unit_amount: 1, // $0.01 per unit
  // Or use tiered pricing:
  // billing_scheme: 'tiered',
  // tiers_mode: 'graduated',
  // tiers: [
  //   { up_to: 1000, unit_amount: 0 },         // First 1000 free
  //   { up_to: 10000, unit_amount: 1 },         // $0.01 each up to 10k
  //   { up_to: 'inf', unit_amount_decimal: '0.5' }, // $0.005 above 10k
  // ],
});

console.log('Metered price ID:', price.id); // Save this as STRIPE_METERED_PRICE_ID

Step 2: Track Usage in Your Database

// prisma/schema.prisma
model UsageRecord {
  id             String   @id @default(cuid())
  userId         String
  subscriptionId String
  metric         String   // 'api_calls', 'storage_gb', 'seats'
  quantity       Int
  recordedAt     DateTime @default(now())

  user User @relation(fields: [userId], references: [id])

  @@index([userId, metric, recordedAt])
}

model Subscription {
  id                    String   @id @default(cuid())
  userId                String   @unique
  stripeSubscriptionId  String   @unique
  stripeSubscriptionItemId String? // Needed for metered billing
  status                String
  currentPeriodEnd      DateTime
  user                  User     @relation(fields: [userId], references: [id])
}

Step 3: Usage Tracking Service

// lib/usage.ts
import { prisma } from './prisma';
import { stripe } from './stripe';

export async function trackUsage(
  userId: string,
  metric: string,
  quantity: number = 1
) {
  const subscription = await prisma.subscription.findUnique({
    where: { userId },
  });

  if (!subscription) return; // No subscription, no tracking

  // Store locally for fast queries
  await prisma.usageRecord.create({
    data: {
      userId,
      subscriptionId: subscription.id,
      metric,
      quantity,
    },
  });
}

export async function getCurrentUsage(
  userId: string,
  metric: string
): Promise<number> {
  const subscription = await prisma.subscription.findUnique({
    where: { userId },
  });

  if (!subscription) return 0;

  const result = await prisma.usageRecord.aggregate({
    where: {
      userId,
      metric,
      recordedAt: {
        gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // Current month
      },
    },
    _sum: { quantity: true },
  });

  return result._sum.quantity ?? 0;
}

Step 4: Report Usage to Stripe

Stripe needs usage reported before invoicing. Two approaches:

Option A: Real-time reporting (simple, more API calls)

// Report to Stripe immediately when usage occurs
export async function trackUsageAndReport(
  userId: string,
  metric: string,
  quantity: number = 1
) {
  const subscription = await prisma.subscription.findUnique({
    where: { userId },
  });

  if (!subscription?.stripeSubscriptionItemId) return;

  // Track locally
  await trackUsage(userId, metric, quantity);

  // Report to Stripe immediately
  await stripe.subscriptionItems.createUsageRecord(
    subscription.stripeSubscriptionItemId,
    {
      quantity,
      timestamp: 'now',
      action: 'increment',
    }
  );
}

Option B: Batch reporting via cron (preferred)

// lib/usage-reporter.ts — run daily or hourly
export async function reportPendingUsage() {
  const unreportedUsage = await prisma.usageRecord.findMany({
    where: { reportedToStripe: false },
    include: { user: { include: { subscription: true } } },
  });

  // Group by subscription + metric
  const grouped = groupBy(
    unreportedUsage,
    r => `${r.user.subscription?.stripeSubscriptionItemId}:${r.metric}`
  );

  for (const [key, records] of Object.entries(grouped)) {
    const [itemId] = key.split(':');
    const total = records.reduce((sum, r) => sum + r.quantity, 0);

    try {
      await stripe.subscriptionItems.createUsageRecord(itemId, {
        quantity: total,
        timestamp: 'now',
        action: 'increment',
      });

      await prisma.usageRecord.updateMany({
        where: { id: { in: records.map(r => r.id) } },
        data: { reportedToStripe: true },
      });
    } catch (err) {
      console.error(`Failed to report usage for item ${itemId}:`, err);
    }
  }
}

// Cron job (Vercel cron or external)
// vercel.json
// { "crons": [{ "path": "/api/cron/usage-report", "schedule": "0 * * * *" }] }

Step 5: Usage in API Routes

// app/api/generate/route.ts — track usage on each API call
import { trackUsageAndReport } from '@/lib/usage';
import { getServerSession } from 'next-auth';

export async function POST(req: Request) {
  const session = await getServerSession();
  if (!session) return new Response('Unauthorized', { status: 401 });

  // Check usage limit before processing
  const currentUsage = await getCurrentUsage(session.user.id, 'api_calls');
  const limit = getPlanLimit(session.user.subscription?.plan, 'api_calls');

  if (currentUsage >= limit) {
    return new Response('Usage limit exceeded', { status: 429 });
  }

  // Process the request
  const result = await processRequest(req);

  // Track usage after success
  await trackUsageAndReport(session.user.id, 'api_calls', 1);

  return Response.json(result);
}

Showing Usage to Users

// components/UsageMeter.tsx
export async function UsageMeter({ userId }: { userId: string }) {
  const current = await getCurrentUsage(userId, 'api_calls');
  const limit = 1000; // Pro plan limit
  const pct = Math.min((current / limit) * 100, 100);

  return (
    <div>
      <div className="flex justify-between text-sm mb-1">
        <span>API Calls</span>
        <span>{current.toLocaleString()} / {limit.toLocaleString()}</span>
      </div>
      <div className="h-2 bg-gray-200 rounded-full">
        <div
          className={`h-2 rounded-full ${pct > 90 ? 'bg-red-500' : 'bg-indigo-600'}`}
          style={{ width: `${pct}%` }}
        />
      </div>
      {pct > 90 && (
        <p className="text-red-600 text-xs mt-1">
          You're at {pct.toFixed(0)}% of your limit.{' '}
          <a href="/billing">Upgrade →</a>
        </p>
      )}
    </div>
  );
}

Time Budget

TaskDuration
Stripe metered price setup1 hour
Usage tracking schema + service1 day
Stripe reporting (cron or real-time)1 day
Usage display UI0.5 day
Total~2.5 days

Compare boilerplates with usage-based billing support on StarterPick.

Check out this boilerplate

View ShipFast on StarterPick →

Comments