# Reporting usage

A meter turns reported usage into a billable quantity. You report events; the
meter's aggregation method decides what the period is billed on. The method is
chosen when the meter is created and freezes once any usage exists against it,
because changing how a quantity is derived would restate periods that have
already been invoiced.

## Reporting an event

```bash
curl -X POST 'https://api.ledgerbee.com/api/v1/meters/report' \
  -H 'x-api-key: <your-key>' \
  -H 'x-api-idempotency-key: acct-42-2026-08-19' \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "0190abcd-1111-7000-8000-000000000001",
    "meterName": "api_calls",
    "value": 250
  }'
```

Identify the meter by `meterId` or `meterName`; one of the two is required. See
the [API Reference](/api) for the full schema.

### The response is an acknowledgement, not a recorded event

Reporting is asynchronous. A successful call answers `202` with
`{"status": "queued", "requestId": "...", "idempotencyKey": "..."}` — the event
is accepted and recorded a moment later. A key you have already used answers
`200` with `"status": "duplicate"` and records nothing further.

So a `202` means the report will be counted, not that it already has. Nothing
downstream needs you to wait for it: the quantity is read when the period is
billed, long after ingestion.

### Retries are safe when the key is derived

The `x-api-idempotency-key` header is required, and it is yours to choose:
reporting the same key twice records one event. Derive it from what you are
reporting — the customer, the meter, and the period or batch. A random key, or
one built from the clock at call time, makes a retry after a network timeout a
second charge, which is what this header exists to prevent.

The same rules apply as everywhere else on the API; see
[Idempotency](/guides/idempotency).

### Which period an event falls in

`timestamp` decides that, and defaults to now. It is the usage instant rather
than the reporting instant, so a nightly job reporting yesterday's consumption
sends yesterday's timestamp.

An event timestamped into a period that has already been invoiced leaves that
invoice unchanged. The period was billed on what the meter held at the time.

## What makes usage billable

Reporting and billing are independent. A meter accepts events for any customer
at any time — there is no subscription check on the way in, so you can report as
much as you like, as early as you like.

Billing reads that usage back one subscription at a time. An invoice covers a
subscription's billing period and bills the usage recorded inside it, so usage
sitting in a stretch of time no subscription covers is stored and never billed:
there is no period to bill it on. A report that returned `202` and never reached
an invoice is almost always this — most often usage recorded before the
subscription started.

### Backdating picks that usage up

Giving a subscription a start date in the past extends its coverage back over
usage you already reported, and the first invoice bills it. That is the default,
and it is what you want when the reporting ran ahead of the paperwork.

You are asked before it happens. Assigning, copying or replacing a subscription
with a past start date lists the usage already recorded in that window, one row
per meter, with the option to discard it instead. Discarding sets a floor on that
subscription and meter: usage recorded before the assignment is excluded from
every invoice, and usage from the assignment onward bills normally. A floor only
ever moves forward, so a repeated or out-of-order discard can never re-admit
units an earlier one excluded.

Meters aggregating on `last` are listed but cannot be discarded. They report a
level the customer currently holds rather than consumption, so flooring their
history would bill 0 for a resource that is still in use.

## Aggregation methods

{/* @codegen meter-aggregations — generated by `pnpm docs:generate`; do not edit */}

| Method | The period is billed on | Use it for |
| --- | --- | --- |
| `sum` | The total of every quantity reported. | Tokens, GB transferred, API calls. |
| `count` | How many events arrived. The quantity is ignored. | Deliveries, runs, webhook sends. |
| `last` | The most recent quantity — a level. | A current seat count. |
| `max` | The highest quantity reported. | Peak seats or connections held across the whole account. |
| `max_per_dimension` | The peak each `dimension` reached, added together. | Per-seat, per-device or per-user peaks. |

{/* @codegen-end meter-aggregations */}

The last two differ in a way worth being deliberate about. `max` bills the
biggest number anyone reported, so for a per-subject charge the busiest subject
sets the price for the whole account. `max_per_dimension` bills what each
subject peaked at, added together.

## Dimensions

A dimension names what the quantity is measured per. Only `max_per_dimension`
reads it; every other method ignores it.

```bash
curl -X POST 'https://api.ledgerbee.com/api/v1/meters/report' \
  -H 'x-api-key: <your-key>' \
  -H 'x-api-idempotency-key: seats-4f21-2026-08-19' \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "0190abcd-1111-7000-8000-000000000001",
    "meterName": "seats_held",
    "dimension": "seat-4f21",
    "value": 3
  }'
```

Reporting a level per subject once a day gives a period total of each subject's
own peak:

| Day | `seat-a` | `seat-b` | Account total that day |
| --- | --- | --- | --- |
| 1 | 1 | 3 | 4 |
| 2 | 4 | 0 | 4 |
| 3 | 1 | 3 | 4 |

`max_per_dimension` bills 7: `seat-a` peaked at 4 and `seat-b` at 3. `max`
bills 4 and `sum` bills 12.

No calculation over the daily totals reaches 7, because the subjects peaked on
different days and the totals no longer hold that. This is why the dimension
rides the event rather than being added up first.

### A dimension is a grouping key

It identifies something that persists across reports, so that "the peak this
subject reached" has a meaning. A value that varies per report — a request id,
an event id, a timestamp — gives every event its own group and bills the maximum
of each event by itself.

Descriptive detail belongs in `metadata`, which is stored with the event and
never aggregated on.

A meter refuses a new dimension once it carries too many distinct values, so an
accidental high-cardinality key surfaces as a rejected report rather than an
invoice that grows without limit. Values already in use keep working, and a
dimension is capped at 100 characters.

### Omitting the dimension

An event with no dimension groups under a single bucket, so a
`max_per_dimension` meter fed by a writer that never sets one bills the same as
`max`. That keeps a misconfigured meter on a defensible number while the
reporting is corrected.

## Required scope and license

Reporting usage needs the `meter-report` scope on the API key
([API keys](/guides/api-keys)) and the Subscription license
([Licenses](/guides/licenses)). A key without the scope gets `403`; a tenant
without the license gets `403` with code `LICENSE.REQUIRED`.
