Subscriptions

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:write also 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).
ts
// 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)
Absolute, not incremental
Set 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.

ts
// 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 — computed unitCount × 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.

ts
// 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.

Usage is not retroactive
Usage recorded after the renewal cron runs for a period is attributed to the next period. Send your final tally before the periodEnd timestamp on the subscription.

Next