Metered Usage
Track and gate usage-based billing with metered features.
This guide walks through implementing usage-based billing: how to define metered features, check balances, report consumption, and handle resets.
Define a metered feature
Metered features track usage against a limit. Define one with type: "metered".
import { feature } from "paykitjs";
const messages = feature({ id: "messages", type: "metered" });Include in plans with different limits
Pass the feature into each plan with a limit and reset interval.
export const free = plan({
id: "free",
name: "Free",
group: "base",
default: true,
includes: [
messages({ limit: 100, reset: "month" }),
],
});
export const pro = plan({
id: "pro",
name: "Pro",
group: "base",
price: { amount: 19, interval: "month" },
includes: [
messages({ limit: 2_000, reset: "month" }),
],
});Free customers get 100 messages per month; Pro customers get 2,000.
Check before consuming
Call check before performing the action. It returns allowed (whether the customer has remaining balance) and balance (how many units are left).
const { allowed, balance } = await paykit.check({
customerId: userId,
featureId: "messages",
});If allowed is false, the customer has hit their limit. Return early instead of proceeding.
Perform the action
Only run the actual work if allowed is true.
if (!allowed) {
return Response.json({ error: "Usage limit reached" }, { status: 403 });
}
const response = await generateChatResponse(input);Report usage
After the action succeeds, call report to decrement the customer's balance.
await paykit.report({
customerId: userId,
featureId: "messages",
amount: 1,
});Pass amount matching however many units the action consumed. For most cases that's 1, but you can pass larger values for batch operations.
Balance resets
PayKit uses lazy resets. When the reset period passes, it doesn't reset balances proactively. Instead, the next check or report call detects that the period has expired and resets the balance automatically before returning.
This means you don't need any cron jobs or scheduled tasks. Resets happen on-demand, exactly when needed.
Complete example
A full API route handler for an AI chat endpoint:
export async function POST(request: Request) {
const { allowed } = await paykit.check({
customerId: userId,
featureId: "messages",
});
if (!allowed) {
return Response.json({ error: "Usage limit reached" }, { status: 403 });
}
const response = await generateChatResponse(input);
await paykit.report({
customerId: userId,
featureId: "messages",
amount: 1,
});
return Response.json(response);
}If you just need on/off access without tracking usage, use boolean type instead. check still returns allowed, but there's no balance to track or reset.