Skip to main content

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 — SMS segments, 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: [...])
Fetch one filtered page — the query string carries the same from / until / types / limit as the curl example below:
Page through a whole window by feeding pagination.cursor back until has_more is false:
The Python SDK is core-scope — it wraps the 8 core resources (messaging, voice, contacts, campaigns, verify, numbers) and reaches everything else through the same escape hatch. See the Python SDK. Raw curl in the body of this page works identically. Full SDK index at SDK quickstart.

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.
The header row lists every column the export writes, in a stable order — parse by header name, not position, so a column added later never shifts your decoder. Empty cells are genuinely null fields (a call has no 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 an AbortController 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 a recording_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.