Managing CDRs
This page covers the operational side of CDRs: pulling records for billing and troubleshooting, exporting them in bulk, and erasing them for retention compliance. Read How CDRs Work first for the call/leg model and the field meanings — the commands below make much more sense knowing that a “call” is a header with leg children.
Conventions as usual: $VSS, $TOKEN, $NSID ($PTID for
partition-wide work) for curl; an active vsscli profile. CDR commands
live under vsscli cdrs … (alias cdr).
Three knobs are shared by every query below:
- Scope — the active namespace by default;
--include-children(curl:?include_children=true) widens to the whole descendant subtree;--partition(curl: the/v1/pt/{ptid}/…variants) fans out over every namespace in a partition. Bare--partitionuses your token’s partition,--partition=<ptid>targets one explicitly. - Range —
--from/--toaccept RFC3339 or unix seconds;--sincetakes a duration (24h,7d). Default: the last 24 hours. - Pagination — an opaque cursor. Each page returns
next_cursor; pass it back (--cursor) for the next page, or let--allfollow the cursor to the end. Page size:--limit, server default 100, cap 1000.
Permissions
Section titled “Permissions”| Action | At namespace scope | At partition scope |
|---|---|---|
cdr:read |
partition-admin, namespace-admin, operator, editor, viewer, system-auditor, billing, cdr-compliance |
partition-admin, operator, editor, viewer, system-auditor, billing, cdr-compliance |
cdr:purge |
partition-admin, cdr-compliance only |
partition-admin, cdr-compliance only |
Note the deliberate asymmetry: namespace-admin reads CDRs but cannot
purge them — erasure stays with the partition owner and the dedicated
cdr-compliance role, mirroring how namespace deletion is withheld. Give
your billing system a service account with the billing role (read-only,
no switch access at all); give your retention job cdr-compliance.
Listing calls
Section titled “Listing calls”The calls view returns headers with their legs nested — the right shape for “show me what happened”:
curl -s "$VSS/v1/ns/$NSID/cdrs?from=2026-07-11T00:00:00Z&limit=50" \ -H "Authorization: Bearer $TOKEN"# → {"success":true,"calls":[{"call_ulid":"01K...","direction":"outbound",# "started_at":"…","ended_at":"…","completed":true,# "legs":[{"leg_uuid":"…","leg_role":"inbound-device",# "disposition":"answered","billable_ms":184220, …}, …]}],# "next_cursor":"eyJlcyI6..."}vsscli cdrs calls --since 24hvsscli cdrs calls --from 2026-07-01T00:00:00Z --to 2026-07-08T00:00:00Z --allThe table shows each call’s legs as child rows, so the LEG_UUIDs are
right there to paste into cdrs get. Long listings honour --pager.
Calls are returned newest-first. A call whose completed is false is
live right now — open records are visible the moment a call starts.
Listing legs — the billing extract
Section titled “Listing legs — the billing extract”The flat legs view is the workhorse for billing files and targeted
searches. It supports a substring filter: any subset of the leg’s
string fields, each matched case-insensitively as a substring, ANDed
together. Filterable keys are the leg columns —
sip_source_user, sip_dialed_user, callerid_number, route_trunk,
disposition, direction, leg_role, attestation, sip_call_id,
and the rest of the string fields on the record.
# Who dialed 3055551234 this week? Filters are plain query parameterscurl -s "$VSS/v1/ns/$NSID/cdrs/legs?from=2026-07-05T00:00:00Z&sip_dialed_user=3055551234&direction=outbound" \ -H "Authorization: Bearer $TOKEN"
# Same query as CSV (note the Accept header)curl -s "$VSS/v1/ns/$NSID/cdrs/legs?from=2026-07-05T00:00:00Z&sip_dialed_user=3055551234" \ -H "Authorization: Bearer $TOKEN" -H "Accept: text/csv" -o legs.csvThe filter can also be sent as a JSON body of the same keys (handy when
an integration builds it programmatically); when both are present, a
query parameter wins over the body’s value for that key. In CSV mode the
next-page cursor arrives in the X-Next-Cursor response header instead
of the body.
vsscli cdrs legs --since 7d --filter sip_dialed_user=3055551234 \ --filter direction=outbound
# Straight to a billing file, following every pagevsscli cdrs legs --since 7d --csv --all --out legs.csv--filter key=value repeats; all filters must match (AND).
For pulling more than a few thousand rows, prefer the bulk export below — it’s resumable and doesn’t re-run the scan per page.
Fetching one record
Section titled “Fetching one record”GET …/cdrs/{id} takes either identifier — a call ULID returns that
call; a leg UUID returns the call(s) containing that leg (two when the
leg straddles a namespace transfer). Transfer links are followed one hop
by default, nesting the counterpart as linked_call:
curl -s "$VSS/v1/ns/$NSID/cdrs/01K02X5RJ8Q9Z3F7M6T1V4W8YA" \ -H "Authorization: Bearer $TOKEN"
# Without link traversalcurl -s "$VSS/v1/ns/$NSID/cdrs/01K02X5RJ8Q9Z3F7M6T1V4W8YA?follow_links=false" \ -H "Authorization: Bearer $TOKEN"vsscli cdrs get 01K02X5RJ8Q9Z3F7M6T1V4W8YA # a call, by ULIDvsscli cdrs get 6f9619ff-8b86-d011-b42d-00c04fc964ff # a leg, by UUIDvsscli cdrs get <ulid> --no-follow-linksA record is only addressable from its own namespace — asking a namespace for a sibling’s record is a 404, and a transfer counterpart’s body is only nested when your grant also covers its namespace.
Partition-wide queries
Section titled “Partition-wide queries”For end-of-cycle billing across every tenant at once, the same three reads exist at partition scope, fanning out over every namespace the partition owns:
curl -s "$VSS/v1/pt/$PTID/cdrs/legs?from=2026-07-01T00:00:00Z&to=2026-08-01T00:00:00Z" \ -H "Authorization: Bearer $TOKEN" -H "Accept: text/csv" -o july.csvvsscli cdrs legs --partition --since 30d --csv --all --out july.csvvsscli cdrs calls --partition=$PTID --since 24hThese are scan-heavy, infrequent-use endpoints — a partition query walks every namespace in the date range. For the monthly billing pull, use the bulk export instead.
Bulk export
Section titled “Bulk export”The export facility is built for the “give me last month, all of it” job: it opens a server-side download session that freezes the query (range, scope, filter, format), then streams the result in ~100 MB chunks. The server tracks the last chunk you acknowledged, so a dropped transfer resumes where it left off instead of starting over. Formats: NDJSON (default) or CSV. Sessions expire after 48 idle hours.
# One file for the whole month, partition-widevsscli cdrs export --partition --from 2026-06-01T00:00:00Z \ --to 2026-07-01T00:00:00Z --format csv --out june.csv
# One JSON file per leg, for an archive bucket (NDJSON only)vsscli cdrs export --since 30d --out-dir ./cdr-archive/
# The export prints its download id when it opens; resume a dropped one:vsscli cdrs export --resume 4c1f2a3e-… --out june.csv
# Inspect / cancel sessionsvsscli cdrs downloads listvsscli cdrs downloads abort 4c1f2a3e-…# 1. Open a sessioncurl -s -X POST "$VSS/v1/ns/$NSID/cdr-downloads" \ -H "Authorization: Bearer $TOKEN" \ -d '{"from":"2026-06-01T00:00:00Z","to":"2026-07-01T00:00:00Z","format":"csv"}'# → {"success":true,"session":{"download_id":"4c1f2a3e-…","state":"open", …}}
# 2. Pull chunks; each response's X-Cdr-Next-Cursor / X-Cdr-Download-State# arrive as HTTP *trailers*. Pass the cursor back to ack + advance.curl -s --raw "$VSS/v1/ns/$NSID/cdr-downloads/4c1f2a3e-…/next" \ -H "Authorization: Bearer $TOKEN" -o chunk1.csvcurl -s --raw "$VSS/v1/ns/$NSID/cdr-downloads/4c1f2a3e-…/next?cursor=<from-trailer>" \ -H "Authorization: Bearer $TOKEN" -o chunk2.csv# …until X-Cdr-Download-State: completed
# Session progress / abortcurl -s "$VSS/v1/ns/$NSID/cdr-downloads/4c1f2a3e-…" -H "Authorization: Bearer $TOKEN"curl -s -X DELETE "$VSS/v1/ns/$NSID/cdr-downloads/4c1f2a3e-…" -H "Authorization: Bearer $TOKEN"Driving the chunk protocol by hand is fiddly (trailer handling); prefer
vsscli cdrs export or an HTTP client with trailer support for
integrations.
Everything about export requires only cdr:read — it is a read, not an
administrative action. Partition-wide exports use the
/v1/pt/{ptid}/cdr-downloads variants (vsscli: --partition).
Purging CDRs
Section titled “Purging CDRs”The range purge permanently erases every CDR whose event time falls in
the window — the enforcement half of a retention policy, and the erasure
tool for compliance requests. It requires the cdr:purge action
(partition-admin or cdr-compliance only), an explicit to cutoff
(a purge with no to is refused with a 400 — the read endpoints’
“last 24 hours” default would erase the newest records), and a
confirmation body; without one the API answers 412 echoing the exact
date window it would erase, plus an example body to re-send. vsscli
builds the confirmation for you after an interactive confirm that shows
the same window:
curl -s -X DELETE "$VSS/v1/ns/$NSID/cdrs?to=2025-07-12T00:00:00Z&from=2020-01-01T00:00:00Z" \ -H "Authorization: Bearer $TOKEN" \ -d "{\"confirm_id\":\"$NSID\",\"confirm_timestamp\":$(date +%s)}"# → {"success":true,"legs_deleted":184223,"calls_deleted":91456}confirm_id must equal the namespace (or partition) id in the URL, and
the timestamp must be within an hour of server time — a stale script
can’t accidentally re-fire.
# Everything older than one year, this namespace onlyvsscli cdrs purge --to 2025-07-12T00:00:00Z --from 2020-01-01T00:00:00Z
# Partition-wide retention sweepvsscli cdrs purge --partition --to 2025-07-12T00:00:00Z --from 2020-01-01T00:00:00ZThere is no undo. Export before you purge if the records feed anything
downstream, and remember CDRs never expire on their own — a scheduled
purge under the cdr-compliance role is the retention mechanism.
Troubleshooting
Section titled “Troubleshooting”| Response / symptom | Meaning | Do |
|---|---|---|
404 CDR not found |
No record with that id in this namespace | Check you’re in the record’s own namespace; transfer counterparts live under their own nsid |
400 missing range bound |
Purge sent without an explicit to cutoff |
Pass --to / ?to= — purge never defaults its upper bound |
412 confirmation required |
Purge sent without (or with a stale) confirmation body | Check the echoed date window, then re-send with confirm_id = the URL’s scope id and a fresh unix timestamp |
400 invalid cursor |
The cursor isn’t one a previous page returned | Restart the listing; cursors are opaque and query-specific |
410 download session gone |
The export session expired (48 h idle) or was aborted | Open a new export; already-downloaded chunks are still valid |
| Empty results for a call you just made | Range/scope miss, not absence | Records appear immediately — check --since, the namespace, --include-children |
A call shows cancelled legs |
Forked dial where another branch answered | Normal — see dispositions |
disposition: interrupted records |
A call-processing node restarted mid-call | Expected for calls cut by the restart; durations are as-of the failure |