---
title: "Usage-based billing"
description: "Charge for what customers use: create a usage price, record usage with idempotency keys, show the running total, and bill it in arrears with the plan fee."
canonical_url: "https://twominutebilling.com/help/developers/recipes/usage-based-billing"
---
# Usage-based billing

> Charge for what customers use: create a usage price, record usage with idempotency keys, show the running total, and bill it in arrears with the plan fee.

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](https://twominutebilling.com/help/developers/getting-started/quickstart).

```bash
export TMBILLING_URL=https://your-api-host
export TMBILLING_API_KEY=sk_test_…
```

```ts [setup.ts]
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.

<code-group>

```ts [TypeScript]
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",
});
```

```bash [curl]
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"
  }'
```

</code-group>

```json [Response]
{
  "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:

<table>
<thead>
  <tr>
    <th>
      <code>
        usage_aggregation
      </code>
    </th>
    
    <th>
      The period's quantity is
    </th>
    
    <th>
      Use it for
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        sum
      </code>
    </td>
    
    <td>
      The total of every event
    </td>
    
    <td>
      API calls, messages sent, GB transferred
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        max
      </code>
    </td>
    
    <td>
      The highest value reported
    </td>
    
    <td>
      Peak seats, peak storage
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        last
      </code>
    </td>
    
    <td>
      The most recent value
    </td>
    
    <td>
      A gauge you report from time to time
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        count
      </code>
    </td>
    
    <td>
      The number of events. <code>
        quantity
      </code>
      
       is ignored
    </td>
    
    <td>
      Discrete actions
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        min
      </code>
    </td>
    
    <td>
      The lowest value reported
    </td>
    
    <td>
      Committed floors
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        avg
      </code>
    </td>
    
    <td>
      The mean of the reported values, rounded to a whole unit
    </td>
    
    <td>
      Average concurrent usage
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unique
      </code>
    </td>
    
    <td>
      The number of distinct <code>
        unique_key
      </code>
      
       values
    </td>
    
    <td>
      Monthly active users
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        weighted_sum
      </code>
    </td>
    
    <td>
      The level times the time it was held, in unit-hours (or unit-days with <code>
        usage_time_unit: "day"
      </code>
      
      )
    </td>
    
    <td>
      GB-hours of storage, seat-hours
    </td>
  </tr>
</tbody>
</table>

<warning title="Report deltas with sum, gauges with last">

If you report a running total and price it with `sum`, you bill the sum of the running totals.
For a customer you report daily, that is about thirty times what they owe.

</warning>

### 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:

<table>
<thead>
  <tr>
    <th>
      <code>
        usage_aggregation
      </code>
    </th>
    
    <th>
      An event means
    </th>
    
    <th>
      The period bills
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        sum
      </code>
    </td>
    
    <td>
      A change: <code>
        +5
      </code>
      
      , <code>
        -2
      </code>
    </td>
    
    <td>
      The highest level in the period
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        last
      </code>
    </td>
    
    <td>
      The new level
    </td>
    
    <td>
      The level at the end of the period
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        max
      </code>
    </td>
    
    <td>
      The new level
    </td>
    
    <td>
      The highest level, the carried-in level included
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unique
      </code>
    </td>
    
    <td>
      A key added, or removed with <code>
        operation: "remove"
      </code>
    </td>
    
    <td>
      The highest count of active keys
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        weighted_sum
      </code>
    </td>
    
    <td>
      A change
    </td>
    
    <td>
      Level × hours held, rounded once per period
    </td>
  </tr>
</tbody>
</table>

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.

<code-group>

```ts [TypeScript]
const customer = await tmb.customers.create(biz.id, {
  email: "maya@example.com",
  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 }],
});
```

```bash [curl]
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": "maya@example.com",
    "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_…
```

</code-group>

```json [Response]
{
  "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.

<code-group>

```ts [TypeScript]
// 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",
});
```

```bash [curl]
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\"
  }"
```

</code-group>

```json [Response]
{
  "object": "usage_record",
  "id": "ur_XXXXXXXXXXXXXXXXXXXXXX01",
  "item": "si_XXXXXXXXXXXXXXXXXXXXXX02",
  "quantity": 1420,
  "timestamp": "2026-10-20T19:12:04.145Z"
}
```

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        item
      </code>
    </td>
    
    <td>
      string, <strong>
        required
      </strong>
    </td>
    
    <td>
      The subscription item (<code>
        si_…
      </code>
      
      ). One subscription can carry two metered items, and this tells them apart.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        quantity
      </code>
    </td>
    
    <td>
      integer, <strong>
        required
      </strong>
    </td>
    
    <td>
      How much, zero or more. With <code>
        count
      </code>
      
       aggregation the value is ignored.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        timestamp
      </code>
    </td>
    
    <td>
      ISO 8601 date-time
    </td>
    
    <td>
      When the usage happened. Default: now. This decides the period it bills in, not when you sent it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unique_key
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      Required for <code>
        unique
      </code>
      
       aggregation: the thing you count once, such as a user id.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        idempotency_key
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      Removes duplicates per item. The same key returns the first record and is not counted again.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        note
      </code>
    </td>
    
    <td>
      string
    </td>
    
    <td>
      Your own note, up to 500 characters. Never printed on a customer document.
    </td>
  </tr>
</tbody>
</table>

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`.

<code-group>

```ts [TypeScript]
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);
```

```bash [curl]
curl -g "$TMBILLING_URL/v1/businesses/$BIZ/usages?subscription=$SUB&invoiced[is]=false" \
  -H "Authorization: Bearer $TMBILLING_API_KEY"
```

</code-group>

```json [Response]
{
  "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](https://twominutebilling.com/help/developers/api-essentials/pagination-and-filtering)).
Add the quantities with your aggregation: for `sum`, the total.

<note title="The renewal estimate leaves usage out">

`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.

</note>

## 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](https://twominutebilling.com/help/guides/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:

<endpoint method="POST" path="/v1/businesses/{biz}/invoices/{inv}/close">



</endpoint>

Closing 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](https://twominutebilling.com/help/developers/concepts/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

<table>
<thead>
  <tr>
    <th>
      Status
    </th>
    
    <th>
      Message
    </th>
    
    <th>
      Fix
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      404
    </td>
    
    <td>
      <code>
        We could not find the subscription item si_….
      </code>
    </td>
    
    <td>
      Send the item id (<code>
        si_…
      </code>
      
      ), not the price id.
    </td>
  </tr>
  
  <tr>
    <td>
      404
    </td>
    
    <td>
      <code>
        We could not find the subscription sub_….
      </code>
    </td>
    
    <td>
      Check the subscription id and that your key's mode matches the business.
    </td>
  </tr>
  
  <tr>
    <td>
      400
    </td>
    
    <td>
      <code>
        We could not record the usage. A negative quantity is allowed only for a recurring sum or a weighted_sum price. …
      </code>
    </td>
    
    <td>
      Send a quantity of zero or more, or make the price recurring.
    </td>
  </tr>
  
  <tr>
    <td>
      400
    </td>
    
    <td>
      <code>
        We could not record the usage. Only a price with the unique aggregation takes a remove operation. …
      </code>
    </td>
    
    <td>
      Send <code>
        operation: "remove"
      </code>
      
       only for a <code>
        unique
      </code>
      
       price, with its <code>
        unique_key
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      409
    </td>
    
    <td>
      <code>
        Usage ur_… was billed on invoice inv_… and cannot be deleted — issue a credit note to correct the charge
      </code>
    </td>
    
    <td>
      An issued invoice already billed the record. Correct it with a credit note.
    </td>
  </tr>
</tbody>
</table>

## Related

- [Usage-based billing features](https://twominutebilling.com/solutions/usage)


## Sitemap

See the full [sitemap](https://twominutebilling.com/sitemap.md) for all pages.
