Usage-based billing
Charge a base fee plus a price per unit of use — API calls, messages, gigabytes. You record usage as it happens. tmbilling adds it up for each billing period and bills it in arrears, on the invoice that closes that period. Recurring lines still bill in advance, so one invoice often carries next month's base fee and last month's usage.
Before you begin
You need a test API key and your base URL, set up as in the Quickstart.
export TMBILLING_URL=https://your-api-host
export TMBILLING_API_KEY=sk_test_…
import { Tmbilling } from "@two-minute-billing/sdk";
const tmb = new Tmbilling({
apiKey: process.env.TMBILLING_API_KEY!,
baseUrl: process.env.TMBILLING_URL,
});
Create a base plan and a usage price
The plan is a flat $49.00 a month. The usage price charges 1 cent (amount_minor: 1) per API
call, and usage_aggregation: "sum" adds up every event in the period.
const biz = await tmb.businesses.create({ name: "Quillmark API", default_currency: "USD" });
const plan = await tmb.products.create(biz.id, { name: "Platform" });
const base = await tmb.prices.create(biz.id, {
product: plan.id,
name: "Platform monthly",
model: "flat",
currency: "USD",
amount_minor: 4900,
interval: "month",
});
const calls = await tmb.products.create(biz.id, {
name: "API calls",
kind: "addon",
unit_of_measure: "call",
});
const perCall = await tmb.prices.create(biz.id, {
product: calls.id,
name: "API calls monthly",
model: "usage",
usage_aggregation: "sum",
currency: "USD",
amount_minor: 1,
interval: "month",
});
curl -X POST $TMBILLING_URL/v1/businesses \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "name": "Quillmark API", "default_currency": "USD" }'
export BIZ=biz_…
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/products \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "name": "Platform" }'
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"product": "platform",
"name": "Platform monthly",
"model": "flat",
"currency": "USD",
"amount_minor": 4900,
"interval": "month"
}'
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/products \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "name": "API calls", "kind": "addon", "unit_of_measure": "call" }'
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"product": "api-calls",
"name": "API calls monthly",
"model": "usage",
"usage_aggregation": "sum",
"currency": "USD",
"amount_minor": 1,
"interval": "month"
}'
{
"object": "price",
"id": "api-calls-monthly",
"product": "api-calls",
"name": "API calls monthly",
"type": "recurring",
"model": "usage",
"usage_aggregation": "sum",
"currency": "USD",
"amount_minor": 1,
"interval": "month"
}
Select an aggregation
The aggregation turns one period's usage events into one billable quantity:
usage_aggregation | The period's quantity is | Use it for |
|---|---|---|
sum | The total of every event | API calls, messages sent, GB transferred |
max | The highest value reported | Peak seats, peak storage |
last | The most recent value | A gauge you report from time to time |
count | The number of events. quantity is ignored | Discrete actions |
min | The lowest value reported | Committed floors |
avg | The mean of the reported values, rounded to a whole unit | Average concurrent usage |
unique | The number of distinct unique_key values | Monthly active users |
weighted_sum | The level times the time it was held, in unit-hours (or unit-days with usage_time_unit: "day") | GB-hours of storage, seat-hours |
sum, you bill the sum of the running totals.
For a customer you report daily, that is about thirty times what they owe.Price usage with tiers or packages
Any of tiered, volume, stairstep, package, percentage and graduated_percentage bills
recorded usage when you give it a usage_aggregation. Without one, it bills the subscription
quantity. Free quantity comes off first, then the model rates the rest. With tiers of 1–1,000 at
$1.00, 1,001–5,000 at $0.50 and 5,001+ at $0.20, a period with 6,200 calls bills:
tiered: 1,000 × $1.00 + 4,000 × $0.50 + 1,200 × $0.20 = $3,240.00.volume: 6,200 × $0.20 = $1,240.00. A quantity equal to a tier's upper limit stays in that tier.
A period with no usage bills nothing under every model, including a tier's flat fee. Use
minimum_amount_minor for a floor. flat and per_unit cannot take an aggregation: use usage.
The invoice line carries rating, with the tier rows that add up to its amount.
Carry a level to the next period
Set usage_recurring: true for seats or storage. The level stays until a new event changes it,
so a period with no events still bills:
usage_aggregation | An event means | The period bills |
|---|---|---|
sum | A change: +5, -2 | The highest level in the period |
last | The new level | The level at the end of the period |
max | The new level | The highest level, the carried-in level included |
unique | A key added, or removed with operation: "remove" | The highest count of active keys |
weighted_sum | A change | Level × hours held, rounded once per period |
A seat swapped inside a period (remove one key, add another) counts once. The level belongs to
the subscription item: a price change on the item keeps it, and a new item starts at 0. You
cannot change usage_aggregation, usage_recurring or usage_time_unit after you create the
price.
Subscribe the customer
Create the customer, save a test card and subscribe them to both prices. The first invoice charges the $49.00 base fee now. The usage item adds nothing yet: usage bills at the end of the period.
const customer = await tmb.customers.create(biz.id, {
email: "[email protected]",
first_name: "Maya",
last_name: "Castillo",
currency: "USD",
});
await tmb.paymentMethods.create(biz.id, customer.id, { token: "4242424242424242" });
const sub = await tmb.subscriptions.create(biz.id, customer.id, {
items: [{ price: base.id }, { price: perCall.id }],
});
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "Maya",
"last_name": "Castillo",
"currency": "USD"
}'
export CUS=cus_…
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/payment_methods \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "token": "4242424242424242" }'
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/subscriptions \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "items": [{ "price": "platform-monthly" }, { "price": "api-calls-monthly" }] }'
export SUB=sub_…
{
"object": "subscription",
"id": "sub_XXXXXXXXXXXXXXXXXXXXXX01",
"status": "active",
"current_period_start": "2026-10-20T19:12:03.772Z",
"current_period_end": "2026-11-20T19:12:03.772Z",
"items": [
{
"id": "si_XXXXXXXXXXXXXXXXXXXXXX01",
"price": "platform-monthly",
"price_model": "flat",
"quantity": 1,
"unit_price_minor": 4900
},
{
"id": "si_XXXXXXXXXXXXXXXXXXXXXX02",
"price": "api-calls-monthly",
"price_model": "usage",
"quantity": 1,
"unit_price_minor": 1
}
],
"latest_invoice": {
"id": "inv_XXXXXXXXXXXXXXXXXXXXXX01",
"status": "paid",
"total_minor": 4900
}
}
Each line of items has its own id (si_…). You record usage against the item, not against the
price, so keep the id of the api-calls-monthly item.
Record usage
Send usage as it happens, or in batches. Give every event an idempotency_key derived from your
own source event: if your pipeline sends the same event again, tmbilling returns the record it
already has and counts it once.
// The client types `items` loosely, so give it its shape here.
const items = sub.items as unknown as Array<{ id: string; price: string }>;
const meter = items.find((i) => i.price === perCall.id)!;
const usage = await tmb.subscriptions.recordUsage(biz.id, sub.id, {
item: meter.id,
quantity: 1420,
idempotency_key: "acct-8813-hour-2026-10-20T10",
});
export ITEM=$(curl -s $TMBILLING_URL/v1/businesses/$BIZ/subscriptions/$SUB \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
| jq -r '.items[] | select(.price == "api-calls-monthly") | .id')
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/subscriptions/$SUB/usage \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"item\": \"$ITEM\",
\"quantity\": 1420,
\"idempotency_key\": \"acct-8813-hour-2026-10-20T10\"
}"
{
"object": "usage_record",
"id": "ur_XXXXXXXXXXXXXXXXXXXXXX01",
"item": "si_XXXXXXXXXXXXXXXXXXXXXX02",
"quantity": 1420,
"timestamp": "2026-10-20T19:12:04.145Z"
}
| Field | Type | Description |
|---|---|---|
item | string, required | The subscription item (si_…). One subscription can carry two metered items, and this tells them apart. |
quantity | integer, required | How much, zero or more. With count aggregation the value is ignored. |
timestamp | ISO 8601 date-time | When the usage happened. Default: now. This decides the period it bills in, not when you sent it. |
unique_key | string | Required for unique aggregation: the thing you count once, such as a user id. |
idempotency_key | string | Removes duplicates per item. The same key returns the first record and is not counted again. |
note | string | Your own note, up to 500 characters. Never printed on a customer document. |
The usage endpoint takes the idempotency key in the body, not in an Idempotency-Key header.
Show the customer their usage so far
List the usage records that no invoice has billed yet. The client does not wrap this endpoint yet,
so the TypeScript calls it with fetch.
const res = await fetch(
`${process.env.TMBILLING_URL}/v1/businesses/${biz.id}/usages?subscription=${sub.id}&invoiced[is]=false`,
{ headers: { Authorization: `Bearer ${process.env.TMBILLING_API_KEY}` } },
);
const open = (await res.json()) as { data: Array<{ quantity: number }> };
const callsSoFar = open.data.reduce((n, r) => n + r.quantity, 0);
curl -g "$TMBILLING_URL/v1/businesses/$BIZ/usages?subscription=$SUB&invoiced[is]=false" \
-H "Authorization: Bearer $TMBILLING_API_KEY"
{
"object": "list",
"data": [
{
"object": "usage_record",
"id": "ur_XXXXXXXXXXXXXXXXXXXXXX01",
"subscription": "sub_XXXXXXXXXXXXXXXXXXXXXX01",
"item": "si_XXXXXXXXXXXXXXXXXXXXXX02",
"price": "api-calls-monthly",
"quantity": 1420,
"timestamp": "2026-10-20T19:12:04.145Z",
"invoice": null
}
],
"has_more": false
}
The list is paginated (see Pagination).
Add the quantities with your aggregation: for sum, the total.
GET /v1/businesses/{biz}/subscriptions/{sub}/renewal_estimate prices the fixed lines of the next
invoice only. Usage for a period is not known until the period closes.Let the period close
You make no call to bill the usage. At the end of the period the billing clock renews the subscription and raises one invoice with the usage of the period that closed and the base fee of the next one, then collects it.
To see it happen in test mode, open the test business in the dashboard and move the clock past
current_period_end with Time Travel. See
Test with Time Travel. An sk_test_ key cannot move the
clock.
Bill into a pending invoice
Some businesses want an invoice that stays open during the period and collects usage as it
arrives. Create the subscription with "create_pending_invoices": true (or set it as the business
default). Each renewal then opens a pending invoice, and tmbilling adds the usage when the
invoice closes. Close it yourself to bill early:
/v1/businesses/{biz}/invoices/{inv}/closeClosing applies discounts, tax and credits, sets the due date and collects the invoice. Only a
pending invoice can be closed.
Correct usage
List and read usage records at GET /v1/businesses/{biz}/usages. You can delete a record with
DELETE /v1/businesses/{biz}/usages/{id} until an issued invoice bills its period. After that it is locked (409), because tmbilling does not change an issued invoice. Bill a late correction as
a one-time charge on the next invoice, or issue a credit note if the customer owes less. See
Invoices.
On a price with usage_recurring, a record before the open period is also locked (409), because
every later level was measured from it. Send a correcting event instead.
When an item is removed, a term resets or a subscription is cancelled immediately, the usage of the open period is billed on the next invoice as a usage line, so it is not lost.
Keep the delay between usage and reporting short. A pipeline that is hours behind is fine. One that is behind across a period end gives you corrections every month.
Check it worked
- Send the same usage event twice: both responses carry the same
id. - The usage list for the subscription shows the records you sent, with
invoice: null. - After the period closes, the invoice has a usage line for the closed period.
Common errors
| Status | Message | Fix |
|---|---|---|
| 404 | We could not find the subscription item si_…. | Send the item id (si_…), not the price id. |
| 404 | We could not find the subscription sub_…. | Check the subscription id and that your key's mode matches the business. |
| 400 | We could not record the usage. A negative quantity is allowed only for a recurring sum or a weighted_sum price. … | Send a quantity of zero or more, or make the price recurring. |
| 400 | We could not record the usage. Only a price with the unique aggregation takes a remove operation. … | Send operation: "remove" only for a unique price, with its unique_key. |
| 409 | Usage ur_… was billed on invoice inv_… and cannot be deleted — issue a credit note to correct the charge | An issued invoice already billed the record. Correct it with a credit note. |
Related
Trials
Give a plan a free trial, start trials with or without a card, change or end a trial early, and know exactly what happens to the subscription when it ends.
Plan changes
Change a subscription's items, learn how the difference is prorated, and decide whether to apply the change now or schedule it for the end of the term.