Bill for usage

Bill for usage — a runnable, end-to-end guide verified against the VINR sandbox.

View as MarkdownInstall skills

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:

StrategyBillable quantityUse for
sumTotal of every incrementAPI calls, GB transferred
last_during_periodMost recent set valueActive seats at period end
maxHighest value seenPeak 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. 18420

With 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

Was this page helpful?
Edit on GitHub

Last updated on

On this page