CDR export & billing reconciliation feed
Finance reconciliation on a usage-based plan comes down to two data feeds: the per-call records behind your voice charges (call detail records, CDR) and the per-message counters behind your messaging charges (billable usage records). Devotel Orbit ships both as API-level surfaces, so your finance stack — not a shared spreadsheet — reconciles invoices. This page covers the three pieces that make up the feed:GET /messages/usage/records— per-category usage counters (Twilio/v1/Usage/Recordsparity), optionally split per country.GET /voice/calls/export— streamed per-call detail records, CSV or JSON.- The Insights › Costs panel that renders the same counters in the dashboard.
GET /usage/cdr and GET /usage/cdr/export.csv, which give you row-level granularity for finance-grade close.
1. Usage records — GET /messages/usage/records
Returns one counter row per billing category over a rolling window: SMS
inbound/outbound, MMS inbound/outbound, and voice inbound/outbound. Each row
carries its category, channel, direction, count, summed price, and currency.
Example record when
country_splits=1:
- SMS counts by segment, MMS and voice by message or call. A multipart SMS bills per segment, so the usage counter matches what the invoice charges.
- Inbound traffic is included. Inbound SMS/MMS and inbound voice are billable categories and appear alongside outbound.
- Unresolvable recipients land in an
UNKNOWNcountry bucket in the split view rather than being dropped — your split totals always reconcile to the category total.
Worked reconciliation against an invoice line
Pull a month of usage records with per-country splits, then match each record against the line items on the corresponding invoice:category and currency — and sum price per group. In Python:
The most common discrepancy in practice is the segment-split mismatch: a 3-segment outbound SMS billed at 0.045 on the usage endpoint (count=3, price matches) but the finance sheet summed one “message” at a flat message rate. Always group by
count (segments) for SMS in the usage-records view — the price field already applies the segment multiplier, so when your invoice price per category matches the usage price, the count is the only reconcilable axis.
When the country split doesn’t add up to the category total, check the UNKNOWN bucket first — it contains records whose destination prefix couldn’t be resolved to a country code, and those are billed, not dropped.
2. Call detail records — GET /voice/calls/export
Exports per-call detail records as a streamed file — CSV by default, JSON on
request — filtered by the same dimensions as the calls list.
CSV column reference
Whenformat=csv (the default), the response is a Content-Disposition: attachment CSV with these columns in stable order:
CSV-specific quirks to expect:
- Multiline fields. Recording URLs are long enough that some CSV parsers fail on line length — use a parser that reads RFC 4180 double-quoted fields properly rather than splitting naively on newlines.
- Currency preservation. Each row carries its own
currency— a tenant that bills in multiple currencies gets rows with mixed currency values. Sum per currency before comparing to the invoice. - Pre-signed URL expiry. The
recording_urlfield carries a URL that expires after its TTL (typically 1 hour). If you need the recording for later audit, download it before the URL expires rather than storing the URL itself. - Empty cells.
sip_responsecan be empty for calls that never reached a SIP leg.recording_urlis empty when no recording was produced.answered_atis empty for unanswered calls. Treat empty strings, not"null", as the absent case.
Row cap and EXPORT_TOO_LARGE
The export is capped at 100,000 rows per request. When a request would exceed the cap it is rejected outright before any bytes stream — a partial file is never written:
- Monthly export for a high-volume tenant. Split into weekly or daily chunks by adjusting
from/to, pull each chunk as a separate CSV, and concatenate client-side. - A single-week export that still hits the cap. Add a
directionorstatusfilter to halve the result set, or pull two halves (inbound + outbound) separately. - Concatenation gotcha. When you concatenate multiple CSVs, strip the header row from every file except the first, or your downstream aggregator double-counts the header as a data row.
Window validation and empty-window edge cases
The endpoint validates two constraints onfrom/to:
tomust be strictly afterfrom— a reversed or equal pair returns 422 withcode: "INVALID_DATE_RANGE".- Both timestamps must be valid ISO-8601 strings — an unparseable value returns 422 with
code: "INVALID_DATE_FORMAT".
format=json), and the response status is 200. Your reconciliation script should treat a header-only CSV as “no calls in this window” rather than as a failure.
Rate-limit posture
The export endpoint is rate-limited at 5 requests per minute per tenant. Exceeding it returns 429 Too Many Requests with aRetry-After header. Back off linearly — the retry window is typically 60 seconds. Bursting exports for the same tenant from concurrent workers will trigger the limit; serialize exports for a single tenant, and if multiple tenants need the same reconciliation pull, run them from separate API keys (the rate limit is per-tenant, not per-key).
3. End-to-end reconciliation: usage records → invoice
A concrete monthly close loop that reconciles the usage-records feed against an invoice: Step 1 — Pull a month of usage records.
For reconciling at the per-message level (not aggregated category counters), switch to the per-record CDR export guide — it covers the row-level
GET /usage/cdr feed, keyset-cursor paging, and a detailed classification table for unmatched diffs.
4. Reconciling the Costs panel against the Billed messages tile
The Insights › Costs page renders the same usage-records feed next to a “Billed messages” summary tile. They intentionally count different things, and the panel explains this inline:- Billable usage records counts SMS by segment and includes inbound traffic.
- Billed messages counts outbound messages that incurred a charge.
5. Role and access notes
The two endpoints on this page are gated by role and scope:
Without the
billing:read scope, both endpoints return 403 Forbidden with code: "INSUFFICIENT_SCOPE". A member with the analyst or operator role can see the Insights › Costs dashboard panel, but cannot call the export or usage-records endpoints directly — the panel renders the same data under the dashboard’s own authorization boundary.
The Insights › Costs panel (Dashboard → Insights → Channel Costs) is visible to Owner, Admin, Developer, Billing, and Analyst roles. It is not role-gated beyond the dashboard access tier — any user who can reach the Insights hub can see the Costs panel, but only roles with billing:read can pull the raw data through the API.
6. Automation recipes
Monthly cron: pull usage records by window
A cron job that pulls the usage-records feed on the first of each month, offset by the invoice period:Monthly cron: pull CDR export by month boundaries
Reverse-ETL into BigQuery
If your finance stack runs in a data warehouse, the reverse-ETL mechanism mirrors rated usage records and voice call detail records to BigQuery on a schedule — the same per-call and per-message contract, landed in tables you own:
Configure the sync from Dashboard → Integrations → Reverse ETL. See the reverse-ETL warehouse exports guide for destination setup, and the reverse-ETL destination model for the schema each table lands with.
Once the tables are in BigQuery, your month-end reconciliation becomes a SQL query:
7. Related cross-surface flows
The usage-records and CDR feeds tie into several other surfaces on the platform:-
Insights hub map — Usage alert rules. The per-category counters you pull from
GET /messages/usage/recordsare the same metrics the Usage alert rules engine watches. When a rule fires (e.g. “SMS outbound spend exceeded $500/day”), the threshold it crossed is the samepricefield you reconcile against. See the insights usage alert rules guide for configuring rules, and the spend anomaly model for how the alert engine’s 10-minute evaluator and the usage-records feed draw from the same source data. -
Insights hub map — Channel Costs. The Insights › Costs panel (
/insights/costs) renders the same usage-records feed grouped by channel. When you pullGET /messages/usage/records?country_splits=1, the per-category totals should match the panel’s per-channel breakdown one-to-one. A mismatch between the panel and the API response for the samedayswindow means the panel is rendering a cached snapshot — wait one evaluation tick (10 minutes) and re-pull. -
Billing alerts and spend caps. The billing alert engine’s
spend_percentandspend_amountthresholds read the same usage counters. When an alert’saction_on_hitispause_outbound, it pauses sending at the billing gate — and the usage record for the day the pause triggered will show the partial-day count that tripped the threshold. See Billing overview — spend alerts. -
Per-record CDR export for row-level reconciliation. The
GET /messages/usage/recordsandGET /voice/calls/exportendpoints return aggregated category counters. When finance needs to answer “which 4,812 specific messages sum to this $73.21 charge?” the CDR usage export guide coversGET /usage/cdrandGET /usage/cdr/export.csv— the per-record feed with keyset-cursor paging, CSV truncation semantics, and a detailed reconciliation classification table for rounding vs. material vs. unpriced diffs.
See also
- CDR usage export for billing reconciliation — per-record feed, keyset-cursor paging, row-level reconciliation
- Billing overview — full billing surface map (wallet, top-ups, plans, invoices, alerts)
- Reverse-ETL warehouse exports — mirror usage data into BigQuery
- Reverse-ETL destination model — table schemas for
usage_eventsandcall_logs - Insights usage alert rules guide — configure thresholds on the same counters
- Billing-gate outbound blocks — recovery paths for every hard stop the billing gate can throw (pause/block flags, provider faults, hold faults)
- Troubleshoot INSUFFICIENT_BALANCE (402) — the wallet pre-flight stop that precedes gate flags