Metered usage (Meters)
How to bill by consumption while Vipter keeps the count, in the shape of Stripe's Billing Meters: create a meter, price the usage on the offer or on the subscription, send usage events, read the open period and what happens when the cycle closes, with curl and Node.js examples, allowances, graduated tiers, billing thresholds and the differences from Stripe.
With metered usage, your system only tells Vipter about each consumption: a call, a GB, a message. Vipter adds it up, applies the allowance and the price you set, and charges the total on the subscription's saved card when the cycle closes. It is the alternative to additional usage charges, where you compute the amount and call POST /v1/subscriptions/{id}/charges.
The endpoints follow the shape of Stripe's Billing Meters (billing.meter, billing.meter_event, event_summaries). What differs is under Differences from Stripe. Every endpoint, with all its parameters, is in the API reference.
When to use
| You want to | Use |
|---|---|
| Keep the count in your system and decide when and how much to charge. | Charge the saved card: POST /v1/subscriptions/{id}/charges with the amount already computed. |
| Just send the events and let Vipter add them up, apply the allowance, the price and the tiers, and charge at the end of the cycle. | This page. |
| Charge right away, on every consumption. | Charge the saved card. Metered usage has no immediate mode: it accumulates and charges at the close, or when a threshold is reached. |
Both paths produce the same kind of charge (subscription_charge) and the same paid order. A subscription can use both.
The model
meter → usage events (meter_event) → usage item (usage_item) → period (usage_period) → charge (subscription_charge)- Meter. One kind of consumption, identified by an
event_name(api_calls,storage_gb). It says how events aggregate:sumadds the values,countcounts the events,lastkeeps the latest value. - Usage event. A record with the customer (
payload.customer_id) and the value (payload.value). On arrival, Vipter attaches the event to the customer's active subscription that has a price for that meter. - Usage item. The price of a meter on a subscription: amount per unit, allowance, tiers, rounding and threshold. It comes from the offer (configured in the dashboard) or is set through the API on the subscription.
- Period. The subscription's current cycle (
current_period_start→current_period_end). While open, the usage is recomputed from the events on every run. When it closes, it becomes a charge. - Charge. A subscription charge with
source: "usage", made once per period, with one line per meter. It becomes a paid order withbilling_reason: "usage".
Step 1: create the meter
curl -X POST https://api.vipter.com/v1/billing/meters \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"display_name": "API calls",
"event_name": "api_calls",
"default_aggregation": { "formula": "sum" }
}'{
"id": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d",
"object": "billing.meter",
"display_name": "API calls",
"event_name": "api_calls",
"default_aggregation": { "formula": "sum" },
"customer_mapping": { "type": "by_id", "event_payload_key": "customer_id" },
"value_settings": { "event_payload_key": "value" },
"status": "active",
"status_transitions": { "deactivated_at": null },
"livemode": true,
"created": 1791100800,
"updated": 1791100800
}The event_name is what events use to find the meter: lowercase letters, digits, _, . and - only, up to 100 characters, unique in the store (a repeat gets 400 event_name_taken). It does not change afterwards; the display_name changes through POST /v1/billing/meters/{id} and is the name shown on the buyer's order line. If your system already sends events to Stripe with stripe_customer_id in the payload, keep it: Vipter accepts that key as an alias of customer_id.
Step 2: price the usage
The price lives in a usage item, one per meter. There are two places for it:
On the offer, in the dashboard. On the offer's page, the metered usage card lists the store's meters and takes the price per unit, the allowance, the tiers, the rounding and the threshold, per currency. Every subscription of that offer inherits the item on the first metered-usage run after it is created (the run happens every 10 minutes). Inheritance copies the row for the subscription's currency or, failing that, the store's currency. It is a copy: changing the price on the offer later does not change subscriptions that already inherited it.
On the subscription, through the API. For a negotiated price, or for a subscription that did not come from an offer with metered usage:
curl -X POST https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/usage_items \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d",
"unit_amount": 0.4,
"included_units": 10000,
"rounding": "up",
"label": "Calls beyond the allowance"
}'{
"id": "usi_9d2e4f6a8b1c3d5e7f0a2b4c",
"object": "usage_item",
"meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"offer": null,
"currency": "brl",
"unit_amount": 0.4,
"included_units": 10000,
"tiers": null,
"rounding": "up",
"billing_threshold": null,
"label": "Calls beyond the allowance",
"source": "api",
"livemode": true,
"created": 1791100900
}unit_amount is the price of one unit, in the currency's minor unit, fractions allowed: 0.4 is R$ 0.004 per call; 50 is R$ 0.50 per GB. The currency is always the subscription's (another one gets 400 currency_mismatch). Posting the same meter again replaces the item; an item you set through the API is never overwritten by the offer's inheritance. DELETE /v1/subscriptions/{id}/usage_items/{itemId} removes it.
Step 3: send the events
curl -X POST https://api.vipter.com/v1/billing/meter_events \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"event_name": "api_calls",
"identifier": "req_01J9X3K7M2",
"payload": { "customer_id": "cust_9d2e4f6a8b1c3d5e", "value": 1 }
}'{
"id": "mev_1a5c9e3b7d2f6a8c0e4b2d6f",
"object": "billing.meter_event",
"event_name": "api_calls",
"identifier": "req_01J9X3K7M2",
"payload": { "customer_id": "cust_9d2e4f6a8b1c3d5e", "value": 1 },
"customer": "cust_9d2e4f6a8b1c3d5e",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"value": 1,
"timestamp": 1791101000,
"livemode": true,
"created": 1791101000
}identifieris the event's idempotency, per meter. Repeating the sameidentifierreturns the event already stored, with200, without counting twice. Use the ID of the request, the job or the record in your database. Withoutidentifier, Vipter generates one, and every call counts.payload.valueis the value added (sum) or kept (last). On acountmeter the value is ignored and each event counts as 1.timestampis optional, in Unix seconds: up to 35 days in the past and up to 5 minutes ahead, otherwise400 timestamp_out_of_range. Without it, the arrival time is used. Thetimestampdecides which period the event falls into.subscriptionin the response says what the event was attached to.nullis an unattached event.
In volume, use the batch: up to 100 events per call on POST /v1/billing/meter_events/batch. Each item is accepted or refused on its own, and results[i] answers events[i] with status accepted, duplicate or error. The response is 200 whenever at least one item got in; 400 only when none did.
const API = 'https://api.vipter.com/v1';
const headers = { Authorization: `Bearer ${process.env.VIPTER_API_KEY}`, 'Content-Type': 'application/json' };
// One event per consumption. `identifier` is the record's ID on your side: resending never counts twice.
export async function reportUsage(record) {
const res = await fetch(`${API}/billing/meter_events`, {
method: 'POST',
headers,
body: JSON.stringify({
event_name: 'api_calls',
identifier: record.id,
timestamp: Math.floor(record.at.getTime() / 1000), // optional; up to 35 days back
payload: { customer_id: record.vipterCustomerId, value: record.calls },
}),
});
const json = await res.json();
if (res.ok) return json; // 201 new, 200 repeated
// 400 with error.code: no_meter_found, meter_inactive, invalid_payload, timestamp_out_of_range. 404: the customer does not exist.
throw Object.assign(new Error(json.error.message), { code: json.error.code, status: res.status });
}
// In batch: up to 100 per call. Handle `results` item by item; `duplicate` is normal on a resend.
export async function reportUsageBatch(records) {
const res = await fetch(`${API}/billing/meter_events/batch`, {
method: 'POST',
headers,
body: JSON.stringify({
events: records.map((r) => ({ event_name: 'api_calls', identifier: r.id, payload: { customer_id: r.vipterCustomerId, value: r.calls } })),
}),
});
const json = await res.json(); // { accepted, duplicates, errors, results[] }
json.results.forEach((r, i) => {
if (r.status === 'error') console.warn('event refused', records[i].id, r.error.code, r.error.message);
});
return json;
}Store each user's cust_… when the subscription is born: it comes in customer of the checkout session and of the subscription. A customer_id that does not exist in the store gets 404 resource_missing.
Step 4: read the period's usage
curl https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/usage \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/usage",
"has_more": false,
"data": [
{
"id": "usp_7b3e9f1c2a8d4e6f0b2d4f6a",
"object": "usage_period",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"status": "open",
"close_reason": null,
"period_start": 1790186400,
"period_end": 1792778400,
"currency": "brl",
"lines": [
{ "meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d", "event_name": "api_calls", "quantity": 12300, "included": 10000, "billable": 2300, "unit_amount": 0.4, "amount": 920 }
],
"amount_total": 920,
"charge": null,
"computed_at": 1791101100,
"closed_at": null,
"livemode": true,
"created": 1790187000
}
]
}The list carries the 12 most recent periods, newest first. The open period is computed at call time, from the events; closed ones come as they were at the close, with charge pointing to the charge. Each line is a meter: quantity is the aggregate, included the allowance, billable what went over it, amount the line's amount in cents. Use this call to show the month's consumption in your product, instead of adding it up on your side.
For a customer's aggregate over any window, including per hour or per day, use GET /v1/billing/meters/{id}/event_summaries?customer=&start_time=&end_time=&value_grouping_window=day. It adds up all of the customer's events on that meter, attached to a subscription or not.
What happens at the end of the cycle
A routine runs every 10 minutes and, for each subscription with usage items:
- Opens the period of the current cycle, if it does not exist yet: from the subscription's
current_period_startto itscurrent_period_end. Onlyactive,trialingorpast_duesubscriptions have an open period. - Recomputes the open period from the events and stores
linesandamount_total. - Closes the period when the cycle ends (
close_reason: "period_end"), when the accumulated amount reaches the threshold (threshold), or when the subscription stops being chargeable (subscription_ended). - Charges the closed period whose
amount_totalis above zero, once only, on the subscription's saved card: a subscription charge withsource: "usage",Idempotency-Keyusage:{period id}, the descriptionUso 2026-09-23 a 2026-10-23and one line per meter with an amount, carrying the meter'sdisplay_name. A period with a zero amount closes without a charge and without an order.
The charge follows the rules of subscription charges:
- Approved:
invoice.paidgoes out, with the order (billing_reason: "usage",external_order_idwith thesch_…), andsubscription_charge.succeeded, with the charge (metadata.kind: "usage"andmetadata.usage_periodwith theusp_…). The period becomesclosedwithchargefilled in. - Declined by the card:
subscription_charge.failedgoes out and, when the provider recorded an order,invoice.payment_failed. Vipter does not retry on its own; the period stays closed withchargepointing to the declined charge. To collect the amount after the subscriber changes the card, usePOST /v1/subscriptions/{id}/chargeswith the period'samount_total. - Under review: the charge stays
pendingand the events go out when the provider decides. - Refused before reaching the card (subscription with no saved card, card at a provider that does not accept charges without the customer, amount below one unit of the currency, store with the plan payment overdue): the period closes with
charge: nulland the usage is not charged later. Onsubscription_endedthe usage already consumed is still charged to the saved card (the subscription's final bill); it stays uncharged only when the card is no longer available.
The subscription's renewal does not change: the recurring fee keeps being charged on the offer's date and amount, and the usage comes in a separate charge. Each approved usage charge counts as one paid order in the store's Vipter plan quota; usage events do not count.
Pricing examples
The calculations below apply to one line. billable is the aggregate minus included_units; the price applies to billable only. Rounding happens once, on the line's total, to whole cents: up rounds up (default), nearest to the closest.
Flat price with an allowance
{ "meter": "mtr_…", "unit_amount": 0.4, "included_units": 10000 }12,300 calls: billable 2,300 × 0.4 = 920 cents, R$ 9.20. 9,000 calls: billable 0, a line with amount 0.
Graduated tiers
{
"meter": "mtr_…",
"unit_amount": 0,
"included_units": 0,
"tiers": [
{ "up_to": 1000, "unit_amount": 0 },
{ "up_to": 10000, "unit_amount": 0.5 },
{ "up_to": null, "unit_amount": 0.3 }
]
}Each tier covers the units between the previous up_to and its own, and the last one must have up_to: null. With tiers, the item's unit_amount is not part of the calculation (send 0). 25,000 units: 1,000 × 0 + 9,000 × 0.5 + 15,000 × 0.3 = 4,500 + 4,500 = 9,000 cents, R$ 90.00. A tier can have a flat_amount, in cents, charged once when any unit falls into it. Tiers count from billable: with included_units: 1000, the first tier starts at unit 1,001 of the consumption.
Rounding
unit_amount: 0.4 and 23 units: 9.2 cents. With rounding: "up", the line is 10 cents; with nearest, 9. Rounding is per line; the period's total is the sum of the already rounded lines.
Aggregations
sum adds value (GB transferred). count counts events and ignores value (calls, messages). last keeps the value of the event with the latest timestamp in the period (active seats, GB stored): send the current total, not the difference.
Billing threshold
billing_threshold, in cents, closes and charges the period before the end of the cycle when the accumulated amount reaches it:
{ "meter": "mtr_…", "unit_amount": 0.4, "included_units": 10000, "billing_threshold": 20000 }When amount_total reaches R$ 200.00, the period closes with close_reason: "threshold", is charged, and a new period opens from that moment to the end of the cycle. The check happens on every run, so the charge can go somewhat past the threshold. With several items on the subscription, the lowest threshold among them applies, over the period's total. Each close is one charge and one order.
Unattached events
On arrival, the event is attached to the customer's subscription that is active, trialing or past_due and has a usage item for the meter; with more than one, to the most recent. payload.subscription_id picks one of them; if the chosen one has no item for the meter, the event stays unattached. The attachment is made once: an event that arrived before the subscription had the item is not charged later.
An unattached event is stored (subscription: null), appears in event_summaries and is not charged. While this happens, Vipter sends billing.meter.error_report_triggered in the 2026-11-01 catalog, at most once per meter and per hour, with reason.error_count unattached events in the current hour. The most common causes:
- The subscription is new and has not inherited the offer's item yet: inheritance happens on the next run, within 10 minutes. Start sending events after
GET /v1/subscriptions/{id}/usage_itemsreturns the item, or create the item through the API right away. - The item exists only on the old offer: a subscription inherits once; after an offer change, check its items.
- The
customer_idbelongs to another customer, or the subscription pays by PIX and has no saved card (it receives the event, but the charge fails at the close).
Deactivating a meter
POST /v1/billing/meters/{id}/deactivate stops accepting events (400 meter_inactive) right away. Open periods keep showing the meter's quantity, but the price becomes zero. Usage items pointing to it keep existing; remove them if you no longer want to see them. There is no reactivation through the API.
Limits
| What | Limit |
|---|---|
Event timestamp | Up to 35 days in the past and 5 minutes ahead. |
| Events per batch | 1 to 100. |
payload | Up to 20 keys; string, number or boolean values. |
event_name, identifier | Up to 100 characters. event_name: a-z, 0-9, _, ., -. |
display_name, label | Up to 250 and 120 characters. |
tiers | 1 to 20 tiers, ascending up_to, the last one null. |
event_summaries window | Up to one year. |
GET …/usage | The 12 most recent periods. |
| Calls | The API's rate limits. |
What the buyer sees
The period's charge becomes an order with the description Uso <start> a <end> and one line per meter, with the meter's display_name and the line's amount. The subscriber sees the order in the customer portal, next to the renewals, and receives the store's purchase confirmation e-mail, when it is active. In the dashboard, the charge appears on the subscription's page with the source Usage, and the subscription's page shows the open period's usage.
If your product shows consumption in real time, read GET /v1/subscriptions/{id}/usage instead of redoing the math: it is the same calculation that goes to the charge.
Differences from Stripe
- There is no metered price (
pricewithrecurring.usage_type: "metered"). The price lives in the usage item of the offer (through the dashboard) or of the subscription (/usage_items), with fractionalunit_amount,included_units,tiers,roundingandbilling_thresholdin the same object. - There is no invoice. The closed period becomes a subscription charge with
source: "usage", charged right away on the saved card, and an order withbilling_reason: "usage". The events areinvoice.paidandsubscription_charge.*, notinvoice.createdandinvoice.finalized. - The allowance is a field.
included_unitsreplaces the free tier;tierscount after it. - The attachment to the subscription is made when the event arrives, not at the close. An unattached event is not charged later.
billing.meter.error_report_triggeredhas a single error type (no_subscription_for_customer) and goes out at most once per meter and per hour.event_summariesreturns alistwithout pagination, only with the windows that have events.- The
payloadacceptsstripe_customer_idas an alias ofcustomer_id; the value must be Vipter'scust_…. - There are no
meter_event_adjustmentsand no meter reactivation.
What to do next
- See every endpoint and every object under Metered usage, in the API reference.
- Receive
invoice.paid,subscription_charge.*andbilling.meter.error_report_triggeredon your server: event catalog. - To charge an amount you computed yourself, follow Additional usage charges.
SaaS: from sign-up to dashboard
A guide with code to charge a user of your SaaS through Vipter: create the checkout session with the user's ID, redirect, confirm the payment through the API or the checkout.session.completed webhook, handle PIX, idempotency, bill the month's usage on the saved card, cancel and change plans, what to store and how to coexist with Stripe.
Event catalog
The event types Vipter sends by webhook, when each one fires and what comes in data.object, in both catalogs (the 21 original names and the 19 Stripe-style ones, with checkout sessions, subscription charges and metered usage), with full examples.