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 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 channelvoice.
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 apaginationobject with an opaquecursorand ahas_moreflag. 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 thevoice:readscope.
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
(
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.
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:
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:- Pull a bounded window from the export endpoint with your filters:
- In your workbook, sum
units × priceper currency per channel — skipping rows with an empty price — over the same window. - 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. - 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’sunitsreturns the aggregate’s per-category counts 1:1. - Watch the truncation header on exports:
X-Export-Truncated: truemeans the window exceeded 20,000 rows — narrow the window and re-pull the remainder, or iterate the paginated feed instead of the CSV.
Where to go next
- CDR export & billing reconciliation feed — the how-to guide: exports, the aggregate counters, and the Costs panel the dashboard renders them in.
- CDR Export & Billing Reconciliation API — the endpoint contract: full parameter and row schemas for this surface.