Skip to main content

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 channel voice.
Each row carries the fields a finance workbook needs to reconcile an invoice: 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 (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: 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:
  1. In your workbook, sum units × price per currency per channel — skipping rows with an empty price — over the same window.
  2. 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.
  3. 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.
  4. 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