---
title: "Meter usage events"
description: "Send usage events from your app, count them with meters, and bill each combination of values from a rate card. Retry-safe keys, batches of 500, late events and decimals."
canonical_url: "https://twominutebilling.com/help/developers/recipes/meter-usage-events"
---
# Meter usage events

> Send usage events from your app, count them with meters, and bill each combination of values from a rate card. Retry-safe keys, batches of 500, late events and decimals.

Your app sends **usage events** as things happen: a request served, a file stored, tokens used.
A **meter** says which events to count and how. A **price on the meter** bills what it counts,
with a rate for each combination of values. You do not need to know subscription item ids: an
event names the customer by a key your app already has.

This is the event path. The older path, one usage record for each subscription item, is in
[Usage-based billing](https://twominutebilling.com/help/developers/recipes/usage-based-billing) and works as before.

## 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).

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

A meter reads one event name. This one adds up the `gb` property of `storage_used` events and
keeps one total for each `region`:

<code-group>

```ts [TypeScript]
const biz = await tmb.businesses.create({ name: "Fernhill Storage", default_currency: "USD" });

const meter = await tmb.meters.create(biz.id, {
  id: "storage_gb",
  name: "Storage written",
  event_name: "storage_used",
  aggregation: "sum",
  value_property: "gb",
  group_by: ["region"],
});
```

```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": "Fernhill Storage", "default_currency": "USD" }'

export BIZ=biz_…

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/meters \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "storage_gb",
    "name": "Storage written",
    "event_name": "storage_used",
    "aggregation": "sum",
    "value_property": "gb",
    "group_by": ["region"]
  }'
```

</code-group>

<table>
<thead>
  <tr>
    <th>
      <code>
        aggregation
      </code>
    </th>
    
    <th>
      The period bills
    </th>
    
    <th>
      Needs
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        sum
      </code>
    </td>
    
    <td>
      The total of the values
    </td>
    
    <td>
      <code>
        value_property
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        count
      </code>
    </td>
    
    <td>
      The number of events
    </td>
    
    <td>
      nothing
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        min
      </code>
      
       / <code>
        max
      </code>
    </td>
    
    <td>
      The lowest or highest value
    </td>
    
    <td>
      <code>
        value_property
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        avg
      </code>
    </td>
    
    <td>
      The total of the values ÷ the number of events
    </td>
    
    <td>
      <code>
        value_property
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        last
      </code>
    </td>
    
    <td>
      The value of the last event
    </td>
    
    <td>
      <code>
        value_property
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unique
      </code>
    </td>
    
    <td>
      The number of different values of one property
    </td>
    
    <td>
      <code>
        unique_property
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        weighted_sum
      </code>
    </td>
    
    <td>
      A level × the hours (or days) it stayed
    </td>
    
    <td>
      <code>
        value_property
      </code>
      
      , <code>
        time_unit
      </code>
    </td>
  </tr>
</tbody>
</table>

- `filters` counts only some events: `{ "region": ["eu", "us"] }` skips every other region.
- `group_by` takes up to 5 property names. A price can set a rate for each combination.
- Several meters can read the same event name. A new meter also counts the events that we already
keep, so you can add one later.
- After the first counted event or the first price, only `name` can change. To count in a
different way, create a new meter.

## Price the meter

A price on a meter takes its aggregation from the meter. `unit_amount_decimal` is the rate in
**minor units**, up to 12 decimal places, per `quantity_divisor` units. This price bills 2 cents
per GB, and 3 cents in `ap-south`:

<code-group>

```ts [TypeScript]
const product = await tmb.products.create(biz.id, { name: "Storage" });
const storage = await tmb.prices.create(biz.id, {
  product: product.id,
  name: "Storage monthly",
  model: "usage",
  currency: "USD",
  interval: "month",
  meter: meter.id,
  unit_amount_decimal: "2",
  rates: [{ match: { region: "ap-south" }, unit_amount_decimal: "3" }],
});
```

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

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": "storage",
    "name": "Storage monthly",
    "model": "usage",
    "currency": "USD",
    "interval": "month",
    "meter": "storage_gb",
    "unit_amount_decimal": "2",
    "rates": [{ "match": { "region": "ap-south" }, "unit_amount_decimal": "3" }]
  }'
```

</code-group>

A combination takes the rate row that names the most values, then the oldest row. A value that no
row names takes the price's own rate, so a new region still bills. Each combination is one invoice
line, rounded half-up to the cent once. Models `tiered`, `volume` and `package` also work on a
meter. A customer can have one live subscription that bills a meter; a second one is refused with
409.

## Name the customer with a usage key

An event names its customer with `customer`: the `cus_…` id, or any **usage key** you add to the
customer. Use an id your app already has, such as a workspace or account id.

<code-group>

```ts [TypeScript]
const customer = await tmb.customers.create(biz.id, {
  email: "billing@example.com",
  currency: "USD",
});
await tmb.paymentMethods.create(biz.id, customer.id, { token: "4242424242424242" });
await tmb.usageKeys.add(biz.id, customer.id, "workspace-4821");
await tmb.subscriptions.create(biz.id, customer.id, { items: [{ price: storage.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": "billing@example.com", "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": "storage-monthly" }] }'

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/usage_keys \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "key": "workspace-4821" }'
```

</code-group>

An event whose key matches no customer is **not lost**: we keep it as unmatched. When you add the
key to a customer, we match those events and bill them. A key belongs to one customer.

## Send events

Each event has its own `key`. A repeat of a key is a `duplicate`: it changes nothing, even with a
different timestamp or value. So you can retry a send whenever you are not sure it arrived. These
routes do not use the `Idempotency-Key` header.

<code-group>

```ts [TypeScript]
const one = await tmb.usageEvents.send(biz.id, {
  key: "upload-7f3a91",
  customer: "workspace-4821",
  event_name: "storage_used",
  properties: { region: "eu", gb: "12.5" },
});

const batch = await tmb.usageEvents.sendBatch(biz.id, [
  { key: "upload-7f3a92", customer: "workspace-4821", event_name: "storage_used", properties: { region: "eu", gb: 3 } },
  { key: "upload-7f3a93", customer: "workspace-4821", event_name: "storage_used", properties: { region: "ap-south", gb: 8 } },
]);
console.log(one.status, batch.created, batch.duplicates, batch.errors);
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/usage_events \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "upload-7f3a91",
    "customer": "workspace-4821",
    "event_name": "storage_used",
    "properties": { "region": "eu", "gb": "12.5" }
  }'
```

</code-group>

```json [Response]
{
  "status": "created",
  "event": {
    "object": "usage_event",
    "key": "upload-7f3a91",
    "customer": "cus_XXXXXXXXXXXX01",
    "customer_key": "workspace-4821",
    "event_name": "storage_used",
    "timestamp": "2026-11-01T09:30:12.000Z",
    "properties": { "region": "eu", "gb": "12.5" },
    "status": "matched",
    "meters": ["storage_gb"],
    "flags": []
  }
}
```

A batch takes 1 to 500 events and answers with one result for each, in order. One bad event does
not stop the others.

### Rules for events

- `timestamp` is RFC 3339 with `Z` or an offset. It can be up to 35 days in the past and 5 minutes
in the future. Outside that, the event is refused with `timestamp_too_old` or
`timestamp_in_future`. With no timestamp, we use the time we receive it.
- A meter's value property must be a number with at most 12 decimal places. More places, or a
value that is not a number, is refused with `invalid_value`. We never round it or read it as 0.
We read a JSON number from its exact text, so `0.123456789012` keeps every digit.
- `properties` is flat: up to 50 keys of string, number or boolean.
- An event name with no meter is kept, with the flag `no_meter`, so a meter that you add later
counts it.

### Rate limit

The limit is per organization, shared by all its API keys: 100 requests a second by default. A
batch is one request, so batches of 500 carry 50,000 events a second. Every answer has
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. A 429 adds `Retry-After`.

## When usage bills

A meter line bills the exact period of the subscription, to the microsecond. Usage closes about
6 minutes after the period ends, and then the renewal invoice bills it. If your app keeps events
for some minutes before it sends them, set `usage_close_delay_minutes` (0 to 24) on the business.
The total wait is never more than 30 minutes.

An event that arrives after its period closed is still billed: it goes on the next invoice as a
line "Usage from 1–31 Oct 2026", dated to its own period.

## Read and correct events

<code-group>

```ts [TypeScript]
const unmatched = await tmb.usageEvents.list(biz.id, { meter: "storage_gb", status: "unmatched" });
await tmb.usageEvents.void(biz.id, { key: "upload-7f3a93" });
```

```bash [curl]
curl "$TMBILLING_URL/v1/businesses/$BIZ/usage_events?meter=storage_gb&status=unmatched" \
  -H "Authorization: Bearer $TMBILLING_API_KEY"
```

</code-group>

You can void an event until an invoice that bills its period is finalized. The event stays on
record with `voided_at`. Send a replacement with a new key. After finalization, issue a credit
note instead.

## On the invoice

Every invoice line has `quantity_decimal` and `unit_amount_decimal` (strings). Read those: a meter
line carries its true quantity and sub-cent rate there, and keeps `quantity: 1` with the line
amount in `unit_amount_minor` for older clients. The line's `rating` names the meter, the group
values and the rate row that billed it.

## Next

- [Bill AI tokens](https://twominutebilling.com/help/developers/recipes/bill-ai-tokens): a rate card per model and token type,
and the SDK helper that turns an AI response into events.


## Sitemap

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