Usage Reporting
Report metered usage against a subscription's current billing period. Usage items are billed on top of the base subscription amount at the next renewal.
How it works
Each usage item is identified by a kind string (e.g. api_calls, storage_gb). There is exactly one row per (subscription, current period, kind) combination.
Reporting the same kind twice replaces the previous value — it is an
absolute upsert, not an increment. The engine computes totalCents = unitCount × unitPriceCents and stores it
on the row.
At renewal the cron sums all usage rows for the period and adds them to the base subscription charge. The base amount and total usage overage appear as separate line items in the renewal invoice.
An immediate plan or seat change (effective: "now")
resets the current period to the moment of the change, so any usage already reported for the
period it closes out moves forward to the new period start and is billed at the next
renewal instead. A waived period forgives everything due for it, usage
included — that usage is never billed later. See
Lifecycle & states
for both.
Required scopes
-
subscription_usage:write— report (upsert) a usage item. -
subscription_usage:read— list usage items.subscription_usage:writealso grants read access.
Report usage
POST /api/v1/subscriptions/{id}/usage
Body fields:
-
kind(string, 1–64 chars, required) — usage category identifier. Use a stable, lowercase slug you control. -
unitCount(integer ≥ 0, required) — the absolute number of units consumed this period. -
unitPriceCents(integer ≥ 0, required) — per-unit price in the smallest currency unit (cents for USD).
// Report a metered usage item for the current billing period.
// One row per (subscription, current period, kind) — calling this twice
// with the same kind replaces the previous count (absolute upsert).
const res = await fetch(
`https://api.usethrottle.dev/api/v1/subscriptions/${subscriptionId}/usage`,
{
method: 'POST',
headers: {
// Secret API key (sk_test_… / sk_live_…). The key is already pinned to
// one application + environment, so do NOT also send X-Throttle-Application-Id
// / X-Throttle-Environment-Id — that returns 400 header_scope_conflict.
'X-API-Key': process.env.THROTTLE_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
kind: 'api_calls',
unitCount: 1500,
unitPriceCents: 2, // $0.02 per call
}),
},
);
const { data } = await res.json();
// data.totalCents === 3000 (1500 × 2) unitCount to the total consumed so far this
period, not the delta. If you called this endpoint earlier with unitCount: 1000 and 500 more units were used, report unitCount: 1500 — not 500.
Usage can't be recorded on an installment plan (a subscription with totalPayments): it has a fixed total, so the call returns 400 invalid_state and stores nothing. Usage is likewise
refused on a cancelled or completed subscription
(422 invalid_state), and an unknown or cross-environment
subscription id is 404 not_found.
List usage
GET /api/v1/subscriptions/{id}/usage
Returns all usage items recorded for the subscription's current billing period.
// Retrieve all usage items for the current billing period.
const res = await fetch(
`https://api.usethrottle.dev/api/v1/subscriptions/${subscriptionId}/usage`,
{
headers: {
'X-API-Key': process.env.THROTTLE_SECRET_KEY,
},
},
);
const { items } = await res.json();
// items = [{ id, subscriptionId, kind, unitCount, unitPriceCents, totalCents, periodStart, periodEnd, createdAt }] Response shape per item:
-
id— UUID of the usage row. -
subscriptionId— parent subscription UUID. -
kind— usage category string. -
unitCount— units reported. -
unitPriceCents— per-unit price. -
totalCents— computedunitCount × unitPriceCents. -
periodStart/periodEnd— ISO 8601 timestamps bounding the current billing period. -
createdAt— when the row was first written or last upserted.
Multiple usage kinds
A subscription can carry any number of independently tracked usage kinds within a period.
At renewal every kind's totalCents is summed and
added to the renewal charge.
// Report multiple usage kinds in the same period.
// Each kind is tracked independently; all are billed at renewal.
await reportUsage(subscriptionId, { kind: 'api_calls', unitCount: 1500, unitPriceCents: 2 });
await reportUsage(subscriptionId, { kind: 'storage_gb', unitCount: 12, unitPriceCents: 50 });
// Later — update api_calls (replaces 1500 with 1800).
await reportUsage(subscriptionId, { kind: 'api_calls', unitCount: 1800, unitPriceCents: 2 });
// Now: api_calls = 1800 × 2¢ = $36.00, storage_gb = 12 × 50¢ = $6.00 Billing at renewal
At renewal the subscription is charged its base amount plus the sum of all usage totalCents for the ending period, and each usage kind appears as its own line item on the renewal
order and invoice. No prorations are applied — the full usage overage is billed in the
renewal cycle it was accrued in. After a successful renewal all usage rows for that period
are finalized; new usage accumulates against the next period.
Each usage line item is written with type: "usage".
It needs no fulfilment (deliveryMethod: "none"),
and its presence does not change the renewal order's own type — the order stays recurring, same as a renewal with no usage on it. usage is written by the renewal job only: it appears in
order responses, never as a line item type you can send — POST
/api/v1/carts/{id}/items, quotes, and manual order line-item edits all
reject it.
periodEnd timestamp on the subscription.
Next
- Managing Subscriptions — pause, resume, cancel, and change plan.
- Subscription Webhooks — event types fired at renewal and state changes.
- Scopes reference —
subscription_usage:read/subscription_usage:writescope details.