Bill for usage
Bill for usage — a runnable, end-to-end guide verified against the VINR sandbox.
Usage-based billing charges customers for what they actually consume — API calls, gigabytes transferred, seats active — instead of a flat fee. This guide takes you from a metered price to a finalized invoice that reflects real consumption, runnable against the VINR sandbox.
OverviewAsk
The flow has four moving parts: a metered price defines how to charge per unit, a subscription attaches that price to a customer, usage records report consumption as it happens, and VINR aggregates those records into an invoice at the end of each billing period.
your server VINR
│ create metered price │
│───────────────────────►│
│ create subscription │
│───────────────────────►│
│ report usage (mbu_) │ ...repeated all period
│───────────────────────►│
│ │ period ends → aggregate
│ invoice.paid (webhook) │◄── invoice finalized & charged
│◄───────────────────────│You report usage continuously; VINR sums it per period, prices it, and bills the customer automatically.
Define a metered priceAsk
A metered price sets billingScheme: 'usage' and an aggregation strategy. Here we charge €0.002 per API request, summed over the month.
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
const product = await vinr.products.create({
name: 'API access',
});
const price = await vinr.prices.create({
product: product.id, // prod_...
currency: 'EUR',
billingScheme: 'usage',
unitAmountDecimal: '0.2', // €0.002 per unit, in minor-unit decimals
recurring: {
interval: 'month',
usageType: 'metered',
aggregateUsage: 'sum', // sum | last_during_period | max
},
});Use unitAmountDecimal (a string) for sub-cent rates. A whole-cent rate like €0.05 can use unitAmount: 5. See Pricing models for tiered and graduated schemes.
Subscribe the customerAsk
Attach the metered price to a subscription. Metered prices have no fixed quantity — billing is driven entirely by reported usage.
const subscription = await vinr.subscriptions.create({
customer: 'cust_8aZ2',
items: [{ price: price.id }], // price_...
});
console.log(subscription.id); // sub_...Report usage recordsAsk
Each time the customer consumes something, post a usage record against the subscription item. Always pass an idempotencyKey so retried requests don't double-count.
async function reportApiCall(subscriptionItemId: string, count: number, requestId: string) {
const record = await vinr.usageRecords.create(
{
subscriptionItem: subscriptionItemId,
quantity: count,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment', // increment | set
},
{ idempotencyKey: `usage-${requestId}` },
);
return record.id; // mbu_...
}The timestamp decides which billing period a record lands in. Backdated timestamps that fall before the current period boundary are rejected — report usage promptly, or batch within the same period.
For high-volume sources, batch records instead of one call per event:
await vinr.usageRecords.createBatch({
subscriptionItem: 'si_4Kp1',
records: [
{ quantity: 1200, timestamp: 1748390400, action: 'increment' },
{ quantity: 980, timestamp: 1748476800, action: 'increment' },
],
});Aggregation & windowsAsk
VINR collapses all records in a period into a single billable quantity using the price's aggregateUsage strategy:
| Strategy | Billable quantity | Use for |
|---|---|---|
sum | Total of every increment | API calls, GB transferred |
last_during_period | Most recent set value | Active seats at period end |
max | Highest value seen | Peak concurrent connections |
Inspect the running total at any time before the period closes:
const summary = await vinr.usageRecords.summaries({
subscriptionItem: 'si_4Kp1',
});
console.log(summary.data[0].totalUsage); // e.g. 18420With action: 'set', each record overwrites rather than adds — ideal for reporting "current seat count" where you don't want a sum.
Invoicing usageAsk
When the billing period ends, VINR finalizes an invoice: it multiplies the aggregated quantity by the metered rate, adds any flat or tiered components, and charges the customer's default payment method. Fulfil or update entitlements from the webhook so it happens exactly once.
export async function POST(req: Request) {
const event = vinr.webhooks.verify(
await req.text(),
req.headers.get('x-vinr-signature'),
);
switch (event.type) {
case 'invoice.paid':
// inv_... — quantity already aggregated and priced
await extendAccess(event.data.subscription, event.data.periodEnd);
break;
case 'invoice.payment_failed':
await startDunning(event.data.customer);
break;
}
return new Response('OK', { status: 200 });
}Test itAsk
In the sandbox, you can fast-forward a subscription to its period boundary to force an invoice without waiting a month.
Report some usage
Post a few mbu_ records against the subscription item with the snippet above.
Advance the clock
In the Dashboard test mode, use Advance billing period on the subscription to trigger immediate aggregation and invoicing.
Confirm the charge
The invoice charges the sandbox default card. Watch for invoice.paid to land on your webhook, then check the line item quantity matches your reported total.
Next stepsAsk
Pricing models
Add tiered, graduated, and package rates on top of metering.
Manage subscriptions
Upgrade, prorate, and cancel metered plans.
Usage records API
Full reference for reporting and summarizing usage.
Last updated on