> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Usage records: the per-record billing feed

> The row-level usage feed behind the wallet ledger — one record per sent message and per completed call, with the billed units and price each debit was computed from. What the feed is, how it differs from wallet transactions, and how to reconcile it.

# Usage records: the per-record billing feed

Every message Orbit sends and every call it completes is written once as a
per-record usage record, and every wallet debit on the ledger is computed
from exactly one of those records. The usage-records feed is the raw,
row-level CDR feed those debits come from — the feed finance reconciliation
reads when the question is "which usage rows stand behind the ledger lines?"

Use it when the aggregate counters on the
[billing and wallet page](/concepts/billing-and-wallet) or the wallet
transaction ledger are too coarse — you need the individual message or call
row, its billed units, and the price it accrued, not a category total.

## What a usage record is

A usage record is the billing system's unit of charge. One record is one
billed event:

* A **message** (`kind: "message"`) — SMS, MMS, WhatsApp, email, RCS, or any
  other messaging channel, inbound or outbound.
* A **call** (`kind: "call"`) — a completed voice call detail record, with
  the synthetic channel `voice`.

Each row carries the fields a finance workbook needs to reconcile an
invoice:

| Field       | Meaning                                                                                                                              |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `id`        | The underlying message or call record id — your join key back to the wallet ledger reference.                                        |
| `kind`      | `message` or `call`.                                                                                                                 |
| `channel`   | The messaging channel, or the literal `voice` on call rows.                                                                          |
| `direction` | `inbound` or `outbound`.                                                                                                             |
| `endpoint`  | The remote party — the recipient of an outbound row or the sender of an inbound row. One address column keeps the CSV shape stable.  |
| `status`    | The message lifecycle status (`queued`, `sent`, `delivered`, …) or the call lifecycle status (`answered`, `completed`, `failed`, …). |
| `units`     | The billed units. SMS bills per segment, so a multipart SMS reports its segment count; every other record bills as one unit per row. |
| `price`     | The price stamped on the record when it was billed, or empty if the record is unpriced.                                              |
| `currency`  | The ISO-4217 currency of the price, empty when unpriced.                                                                             |
| `ts`        | The billing event time — newest first.                                                                                               |

Records span both inbound and outbound traffic, because both are billable on
a usage plan. A record stays on the feed even when it failed to rate — its
`price` is empty rather than the row disappearing — so the feed always
counts the same rows the aggregate counters count.

## The two read surfaces

The same rows are exposed on two endpoints over the shared base path
`/api/v1/usage-records`:

* `GET /api/v1/usage-records` — the paginated, filterable feed. Returns one
  page of rows plus a `pagination` object with an opaque `cursor` and a
  `has_more` flag. Authentication is any valid session or API key.
* `GET /api/v1/usage-records/export.csv` — a bounded CSV export of the same
  rows in one file, capped at 20,000 rows per request. Exporting is a bulk
  pull of billing rows across the whole organization, so it requires the
  owner, admin, or developer role with the `voice:read` scope.

Both surfaces take identical filter parameters, so a CSV export always pulls
exactly what the paginated feed would page through. The export stamps the
download in the audit history as well, so bulk data egress is discoverable
after the fact.

The export headers report truncation instead of silently dropping rows: when
the matching row count exceeds the cap, the response carries an
`X-Export-Truncated: true` header. Narrow the `since`/`until` window or add
a filter, and split the pull — the same month-by-month approach as the
voice CDR export.

## How a row maps to the wallet debit

Orbit bills **per feed row**. Each message row and each call row is rated
once, and the resulting wallet debit's ledger reference names the
underlying record id — so the row-level feed and the ledger are two views of
the same charges, one shaped for billing navigation and one shaped for
record-level lookup:

* The [wallet transaction ledger](/concepts/wallets-credits-and-charges)
  (`GET /api/v1/billing/transactions`) is the append-only money view: credit
  and debit movements with their ledger references.
* This usage-records feed is the **per-event record view**: the channel,
  direction, endpoint, status, billed units, and priced amount of the exact
  event the debit was computed from.

Because charges post per row, summing `units × price` over a filtered window
reconciles to the debits over the same window, and the aggregate counter
surface (`GET /api/v1/messages/usage/records`) rolls the same rows up per
channel and direction. Unpriced rows need care in that sum: skip rows whose
`price` is empty, and decide in your workbook whether an empty price is a
failure to rate or free-of-charge traffic before reconciling.

## Filters and pagination semantics

Both surfaces accept the same filter set, as query parameters:

| Parameter         | Type     | Notes                                                                                                                                                                                              |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kinds`           | CSV      | `message` and/or `call`. Unrecognized tokens are dropped, not an error.                                                                                                                            |
| `channels`        | CSV      | Channel values from the inbox channel vocabulary (`sms`, `whatsapp`, `email`, `rcs`, `voice`, …). `voice` also selects the call rows, so omitting it excludes calls.                               |
| `statuses`        | CSV      | One open-vocabulary key over both record kinds: message statuses and call statuses. An unrecognized status matches zero rows rather than failing the request.                                      |
| `endpoint`        | string   | The remote party — matches a row when the endpoint is the recipient of an outbound record or the sender of an inbound one, so a "usage charged to this subscriber" lookup returns both directions. |
| `direction`       | string   | `inbound` or `outbound`.                                                                                                                                                                           |
| `since` / `until` | ISO-8601 | Bounding window on the billing event time. `until` must be on or after `since`.                                                                                                                    |
| `limit`           | integer  | Feed: rows per page, 1–100 (default 25). Export: rows in the file, capped at 20,000 (the export default).                                                                                          |
| `cursor`          | string   | Feed only — the opaque continuation token from the previous page's `pagination.cursor`.                                                                                                            |

Pagination is a **keyset cursor over `ts` then record id**, newest first.
Take the cursor string exactly as `pagination.cursor` returns it — it is
opaque, and its internals are not part of the contract. A malformed or
expired cursor never hard-fails the feed: it degrades to the first page of
results instead of an error, so a saved token going stale yields a complete
re-fetch, not a broken pull. There is no offset pagination; the cursor
keeps long scans stable against rows arriving while you page.

CSV parameter multi-select is lenient everywhere: unknown `kinds`,
`channels`, or `statuses` tokens are ignored, so a stale filter value from
an older client narrows the result instead of failing it.

## Reconciliation flow

The billing-team loop, end to end:

1. Pull a bounded window from the export endpoint with your filters:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/usage-records/export.csv?since=2026-08-01T00:00:00Z&until=2026-08-31T23:59:59Z&kinds=call,message" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -o august-usage-records.csv
```

2. In your workbook, sum `units × price` per currency per channel —
   skipping rows with an empty price — over the same window.
3. Pull the wallet ledger (`GET /api/v1/billing/transactions`) for the same
   window, and join each ledger debit's reference back to a record row by
   the record id. The two views agree row-for-row because the charges post
   per feed row.
4. Cross-check the totals against the aggregate counter surface
   (`GET /api/v1/messages/usage/records`), which rolls the same rows up per
   channel and direction. Summing the export's `units` returns the
   aggregate's per-category counts 1:1.
5. Watch the truncation header on exports: `X-Export-Truncated: true` means
   the window exceeded 20,000 rows — narrow the window and re-pull the
   remainder, or iterate the paginated feed instead of the CSV.

For interactive triage rather than bulk pulls, page the JSON feed with the
same filters and inspect rows one screen at a time — the export is the
deliberate bulk-extract surface, the feed is the interactive one.

## Where to go next

* [CDR export & billing reconciliation feed](/billing/cdr-export-billing-reconciliation) — the how-to guide: exports, the aggregate counters, and the Costs panel the dashboard renders them in.
* [CDR Export & Billing Reconciliation API](/api-reference/cdr-export) — the endpoint contract: full parameter and row schemas for this surface.
