Charge per API call in Next.js, with overages billed to Stripe
Meter every call, enforce the quota in the same request, and bill what goes over. The whole loop in one route handler.
In short
Record usage and check the quota in a single call with bb.usage.record, return 429 when the workspace is out, and let overages flow to the subscription. The metering and the enforcement are the same operation, which is what stops the two drifting apart.
Usage-based billing looks simple until the first customer disputes an invoice. Then you find out what your meter actually counted, and what it quietly dropped.
The failure is almost always the same shape: usage is recorded in one place and enforced in another, and the two disagree. This guide keeps them in one call.
Before you start
A BuildBase project with a plan that defines a quota. This guide uses the slug
api-calls. You will also need Stripe connected for the overage half to mean
anything.
Step one: record and check in the same call
The important line is that bb.usage.record returns what is left. You are not
recording usage and then asking a second system whether the customer is allowed
to be here.
// app/api/generate/route.ts
import BuildBase from '@buildbase/sdk';
const bb = BuildBase({ serverUrl, orgId, getSessionId });
export async function POST(request: Request) {
const { workspaceId, prompt } = await request.json();
const usage = await bb.usage.record(workspaceId, {
quotaSlug: 'api-calls',
quantity: 1,
});
if (usage.available <= 0) {
return Response.json({ error: 'Quota exceeded' }, { status: 429 });
}
// ... do the actual work
return Response.json({ ok: true, remaining: usage.available });
}What you should see: call it once and remaining drops by one. Call it past the
plan limit and you get a 429 rather than a bill nobody agreed to.
Step two: charge by weight, not by request
A request that runs for 40ms and one that runs for 12 seconds are not the same product. If your costs scale with work, meter the work.
const usage = await bb.usage.record(workspaceId, {
quotaSlug: 'api-calls',
quantity: tokensUsed,
});quantity is the only thing that changes. The quota, the enforcement and the
billing all follow it.
Warning
Record usage after the work succeeds, not before, unless you are willing to bill for your own failures. If the work can fail halfway, decide deliberately which side of the failure the meter sits on. Customers forgive an outage. They do not forgive being billed for one.
Step three: credits, when the unit is not a request
Some products sell credits rather than calls. Same idea, different primitive, and the error tells you what you need to tell the user:
try {
await bb.credits.consume(workspaceId, {
amount: 10,
description: 'AI generation',
});
} catch (err) {
if (err.code === 'INSUFFICIENT_CREDITS') {
// err.available and err.requested are both on the error
}
}Returning "you need 10 and have 3" is a support ticket you never receive.
The failure modes worth knowing
Metering in middleware. Tempting, and wrong for anything expensive. The middleware does not know whether the work succeeded.
Retries. A client that retries a failed request meters twice unless the retry is idempotent. If your customers integrate with automatic retries, this is the invoice dispute you will actually get.
Trusting the client. quantity comes from your server after the work, never
from the request body. Otherwise the quota is a suggestion.
Enforcing only in the UI. The gate in the interface is a courtesy. The gate in the route handler is the one that counts. The UI half is covered in enforcing quotas in React.
Install
npm i @buildbase/sdk