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=…kinds picks message and/or call; channels takes the inbox vocabulary (sms, mms, whatsapp, email, voice — voice 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=…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-Disposition —
attachment; filename="<stem>-YYYY-MM-DD.csv"; the optionalfilenamequery parameter overrides the stem (the date stamp is appended either way). - Truncation signal — an
X-Export-Truncated: trueheader 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:readscope (the JSON feed is any authenticated session), and it is rate-limited to 5 requests per minute.
Idempotency-Key; on this page every retained retry of either GET returns the same artefact with no double cost.)
cURL
id, kind, channel, direction, endpoint, status, units, price, currency, ts:
4. Reconcile against the per-category aggregate
Sum the export’sunits 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.