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: sumandbilling_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
| Task | Duration |
|---|---|
| Stripe metered price setup | 1 hour |
| Usage tracking schema + service | 1 day |
| Stripe reporting (cron or real-time) | 1 day |
| Usage display UI | 0.5 day |
| Total | ~2.5 days |
Compare boilerplates with usage-based billing support on StarterPick.
Check out this boilerplate
View ShipFast on StarterPick →