Skip to main content

Worked reconciliation chains

One row per billed event: a message (kind: "message") or a call detail record (kind: "call"). The row-level feed below is the parent of the per-category aggregate — the chains here cover read → page → export → reconcile. Copy a request as written and compare the response envelope.

1. Read the JSON feed with filters

GET /api/v1/usage-records?since=…&until=…&kinds=…&channels=…
Every filter narrows the same row set on both arms (messages and calls). kinds picks message and/or call; channels takes the inbox vocabulary (sms, mms, whatsapp, email, voicevoice selects the calls arm); statuses accepts the open union of call and message statuses; endpoint matches the remote party; direction narrows to inbound or outbound; since/until is an ISO-8601 window on the billing event time.

2. Page through the cursor

has_more in data.pagination answers whether another page exists; pass data.pagination.cursor back as the cursor query parameter. A replayed or tampered cursor degrades to the first page again rather than failing, so polling restarts always make progress.
cURL

3. Export the same window as a CSV

GET /api/v1/usage-records/export.csv?since=…&until=…&filename=…
The export repeats the feed with the same filter set, bounded to 20,000 rows (pass a smaller limit to bound it further). It differs from the JSON feed in four ways you build on:
  • Content type — the body is text/csv; charset=utf-8, not JSON (res.json() would throw; read the body as text).
  • Content-Dispositionattachment; filename="<stem>-YYYY-MM-DD.csv"; the optional filename query parameter overrides the stem (the date stamp is appended either way).
  • Truncation signal — an X-Export-Truncated: true header tells you the window exceeded the cap, so split it (e.g. day-by-day) instead of reconciling a partial snapshot.
  • Higher gate — export requires an owner, admin, or developer role with the voice:read scope (the JSON feed is any authenticated session), and it is rate-limited to 5 requests per minute.
Both the JSON list and this export are GET reads: they never create a resource, so an idempotency key is unnecessary on them. (POST endpoints that create resources accept an Idempotency-Key; on this page every retained retry of either GET returns the same artefact with no double cost.)
cURL
The CSV columns are id, kind, channel, direction, endpoint, status, units, price, currency, ts:

4. Reconcile against the per-category aggregate

Sum the export’s units column per (channel, direction, currency) and compare against GET /api/v1/messages/usage/records, the aggregate sibling on the usage page that rolls the same underlying rows up per category — the numbers match 1:1 for the same time window, so a discrepancy means the filters drifted, not the ledger. For per-record billing reconciliation with line-item timing see the CDR export feed; this endpoint is the per-record companion feed that takes its window from the billing event time and returns directly comparable sums.