CDR Export & Billing Reconciliation API
Every finished message (SMS, MMS, WhatsApp, email, RCS, …) and every finished call in your workspace is recorded with the exact unit it was billed on — SMSsegments, voice duration_seconds, the persisted price + currency the wallet was charged at send or call-close time, and terminal timestamps. This surface returns those records as one flat, recency-ordered feed you filter, page, and export.
Use it to reconcile delivered usage against the billed record row-for-row. Reporting only: the feed never re-rates a record, never mutates a wallet, and never touches a provider.
Base path: /usage
Authentication: An organization API key (sent as X-API-Key or Authorization: Bearer dv_live_sk_…), or a session JWT for dashboard integrations. Either way, the caller must hold an owner, admin, or developer role with the billing:read scope — this feed is bulk billing data over the whole workspace, so both gates apply on both endpoints.
Using the SDKs
This page’s endpoints have no typed helper yet — none of the SDKs wraps/usage — so each language reaches them through the same generic escape hatch, which keeps auth, retries, and the response envelope identical to the typed resources. The samples below run the same call as the curl further down the page.
- Node.js (
@devotel-orbit/node) —orbit.request<T>(method, path) - Python (
orbit_sdk) —client.request(method, path, params={...}) - Go (
orbit-go) —client.Request(ctx, method, path, query, body, out, idempotencyKey) - Ruby (
orbit_sdk) —client.request(method, path, query: {...}) - PHP (Devotel\Orbit) —
$client->request(method: ..., path: ..., query: [...])
- Node.js
- Python
- Go
- Ruby
- PHP
- CSV download (Node)
Fetch one filtered page — the query string carries the same Page through a whole window by feeding
from / until / types / limit as the curl example below:pagination.cursor back until has_more is false:Export a JSON page
Page through CDRs
GET /usage/cdr
Returns one page of records, newest first, scoped to the calling tenant. Filtered with the same query string as the CSV export below, so the two formats always agree on which rows match.
Query parameters
Row fields
Every row carries the billing and ops fields needed for line-level reconciliation — endpoints, statuses, prices, never free-text message bodies:Page forward by passing the opaque
pagination.cursor from each response as the cursor query parameter. A malformed cursor degrades to the first page of the window rather than an error.Export a CSV file
Download CDRs as CSV
GET /usage/cdr/export.csv
The same filter set as the JSON feed, returned as a downloadable CSV attachment. Rate-limited to 5 requests per minute and capped at 50,000 rows per pull; page through the JSON endpoint for larger windows.
Additional query parameters
Every export is audit-logged as
usage.cdr_exported with the row count, window, and truncation flag.
segments; a message has no started_at), not missing values.
When a pull exceeds the per-page cap, the response sets
X-Export-Truncated: true — page through with the JSON endpoint to drain the window.Abort a download in progress
There is no export job to cancel on the wire — the CSV streams directly in the response body, so “cancel” happens entirely client-side. Drop the connection (close a curl/socket, or use anAbortController with fetch) and the server stops emitting. A partial download is safe: rows arrive terminated by \n, so the file stays well-formed up to the last complete row — discard the trailing partial line if you cut mid-transfer. No response body is written on abort (equivalent to 204 No Content); per the API convention, a no-content outcome says so here rather than fencing an empty body.
Use the same guard when you only want a truncation probe: read the headers of a GET /usage/cdr/export.csv, check X-Export-Truncated, and abort before the body streams if it says true.
Signed recording URLs inside call rows
Rows for recorded calls carry arecording_url when one exists. The API signs any private recording storage URL before the export leaves the platform, so you never see internal storage paths — you download the file with plain HTTPS and no credential of your own. Those URLs are time-limited: download recordings as part of the same run that pulls the export; refreshing an old export URL after it has expired yields a 4xx from the storage layer. A URL that fails to sign comes back null rather than breaking the export. Exports return only the calling tenant’s own rows — there is no cross-tenant read path.