How CDRs Work
Every call the switch handles produces call detail records (CDRs): a durable, per-namespace account of who called whom, what was dialed, what answered, how long it lasted, and how it ended. CDRs are the billing and audit source of truth — this page explains the data model and the lifecycle so the numbers can be read with confidence. Managing CDRs covers querying, exporting, and purging them.
The model — one call, many legs
Section titled “The model — one call, many legs”A CDR is not one flat row per call. The switch records two kinds of record:
- The call header — one row per call per namespace, carrying the call’s identity, direction, start/end, and its link to a related call (see transfers below).
- Leg records — one row per SIP channel that participated. The caller’s channel is a leg; every device or trunk the switch dialed is its own leg.
A subscriber dialing another subscriber produces two legs (caller in, callee out). A DID ringing a line whose devices hunt across three phones produces four: one inbound trunk leg and three outbound device legs — two of which will close unanswered when the third picks up. All the billing arithmetic (durations, answer state, disposition) is per leg; the header ties them together.
Each leg carries a role describing what it was:
leg_role |
The channel |
|---|---|
inbound-device |
A subscriber device placing a call into the switch |
inbound-trunk |
A call arriving from a carrier/peer trunk |
outbound-device |
The switch ringing a subscriber device |
outbound-trunk |
The switch dialing out over a trunk |
Announcement and music-on-hold channels the switch conjures internally
(role synthetic) do not produce CDR legs — only channels with a real
party on the far end are recorded.
Two direction fields exist, and they answer different questions. The
per-leg direction is the switch’s view of that channel (inbound for
the dial-in legs, outbound for everything dialed). The call header’s
direction is the whole call classified by its first leg: a call
arriving over a trunk is an inbound call; a call placed by a subscriber
device is an outbound call — even when it terminates on-net.
Identifiers
Section titled “Identifiers”- Call ULID (
call_ulid) — 26 characters, time-ordered: sorting ULIDs sorts calls by start time. Minted at the network edge when the INVITE arrives and carried through every layer of the switch, so the same ULID identifies the call in CDRs and in the route-forensics log. - Leg UUID (
leg_uuid) — a standard UUID per leg.
Either identifier can be pasted into the single-record fetch
(vsscli cdrs get <id>) — the format tells the API which one it is.
When records are written
Section titled “When records are written”CDR rows are written open and finalized in place — a record exists from the moment the call does, not only after it ends:
- Call start — the header row is written with
completed: false. - Leg attach — the instant a channel joins the call, its leg row is
written with everything known at setup (parties, dialed numbers,
caller ID, trunk, STIR/SHAKEN attestation) and
completed: false. A CDR query during a live call shows these open rows. - Answer — the leg row is updated with
answer_start,billing_start, and the negotiatedcodec. - Hangup — the leg’s RTP quality statistics are sampled once and
folded in; the leg is then finalized:
event_end,disposition,q850_cause,hangup_originator, and the computed durations land, andcompletedflips totrue. - Call close — after the last leg ends, the switch lingers briefly (a ~2-second settle window, capped at 15 seconds) so trailing media statistics are captured, then closes the header. Any leg that never saw its own hangup event is defensively finalized here — no live call ever leaves a dangling record.
So the practical timing rule: open records are visible immediately, and final records are queryable within a few seconds of hangup.
Durations and billing fields
Section titled “Durations and billing fields”| Field | Measures |
|---|---|
event_start → event_end |
The leg’s full life, setup to teardown |
total_ms |
Exactly that span, in milliseconds |
answer_start / billing_start |
The answer instant (billing_start resets on a namespace transfer — see below) |
billable_ms |
billing_start → event_end; 0 for unanswered legs |
ringing_start_time |
When the far end first alerted (180 Ringing) on an outbound leg — subtract event_start for post-dial delay |
Billing systems should bill from billable_ms on the leg that faces the
paying party; total_ms exists for utilization and troubleshooting.
Dispositions
Section titled “Dispositions”Every finalized leg carries a disposition — the outcome in one word —
plus the raw Q.850 release cause (q850_cause) and which side ended it
(hangup_originator: remote when the far end hung up first, local
for everything the switch or this side initiated).
| Disposition | Meaning | Typical Q.850 |
|---|---|---|
answered |
The leg was answered and later cleared normally | 16 |
busy |
Far end busy | 17 |
no-answer |
Rang out / no response | 18, 19, 20 |
rejected |
Far end refused the call | 21 |
cancelled |
A forked branch that lost the race (answered elsewhere) | 26 |
abandoned |
Caller gave up before answer | 16 (before answer) |
congestion |
No circuit/route available | 34, 38, 41, 42, 44, 47 |
failed |
Any other failure | other |
interrupted |
Closed by crash recovery or node shutdown | 41 |
transferred-out |
Segment closed because the call crossed into another namespace | — |
The cancelled row is worth internalizing: a three-device hunt that gets
answered produces one answered leg and two cancelled legs. That is
normal, not a fault — count calls by header or by the inbound leg, not
by leg rows.
Namespace transfers — linked records
Section titled “Namespace transfers — linked records”When a call crosses from one namespace into another (a DID delivered from a parent’s trunk into a child tenant, for instance), each namespace gets its own complete record — its own call header under its own call ULID, with its own leg rows. Billing separation demands it: neither tenant should need (or be allowed) to read the other’s half to account for their own.
The mechanics:
- The original call’s still-open legs are closed
transferred-outin the source namespace, and its header is completed. - A fresh call ULID is minted in the target namespace; the surviving
physical channels are re-opened under it as a new billable segment —
billing_startresets to the transfer instant, so the two segments’billable_msadd up to the real total without double-billing. - The two headers point at each other:
linked_ulidcarries the counterpart’s ULID andlink_reasonistransfer.
The single-record API follows that link by default (one hop), nesting
the counterpart as linked_call — but only when the caller is also
authorized to read CDRs in the counterpart’s namespace. Otherwise the
link ID is visible, the record body is not.
Field reference
Section titled “Field reference”Call header
Section titled “Call header”| Field | Meaning |
|---|---|
call_ulid |
The call’s identity in this namespace |
nsid / partition_id |
Owning namespace and partition |
direction |
Whole-call direction (inbound / outbound) |
started_at / ended_at |
Call-level bounds |
linked_ulid / link_reason |
Transfer counterpart, when any |
completed |
false while the call is live |
cdr_schema_version |
Record-format version stamp |
Leg — identity and topology
Section titled “Leg — identity and topology”| Field | Meaning |
|---|---|
leg_uuid |
This leg |
call_ulid |
The call (and namespace segment) it belongs to |
parent_leg_uuid |
The leg this one was spawned to serve: every outbound leg — hunt branch, forward target — points at the call’s inbound leg; empty on the inbound leg itself |
leg_role / direction |
See the model |
node_id |
The call-processing node that handled it |
nsid / partition_id |
Owning namespace / partition |
resource_type / resource_id |
The configured resource this leg maps to (a line, trunk, ring group, …) |
Leg — parties and routing
Section titled “Leg — parties and routing”| Field | Meaning |
|---|---|
sip_source_user / sip_source_realm |
The calling party as it entered (device local-part or trunk From user, and its realm) |
sip_source_addr |
Source signalling address (transport:ip:port) of an inbound leg |
sip_dialed_user / sip_dialed_realm |
The called number/user as placed on this leg |
sip_dialed_addr |
The real egress address the edge dialed (outbound legs) |
dialed_before / dialed_after |
The dialed number before and after translations — the audit trail for number manipulation |
route_trunk |
The trunk selected for an outbound trunk leg |
Leg — presented identity
Section titled “Leg — presented identity”| Field | Meaning |
|---|---|
callerid_name / callerid_number |
The caller ID presented on this leg |
pai |
Raw P-Asserted-Identity header (forensic) |
attestation |
STIR/SHAKEN attestation level (A/B/C) |
sip_identity |
The full SIP Identity header received or stamped |
sip_call_id |
This leg’s SIP dialog Call-ID — the join key to packet captures and carrier tickets |
Leg — timing and outcome
Section titled “Leg — timing and outcome”event_start, event_end, answer_start, billing_start,
ringing_start_time, total_ms, billable_ms — see
durations; answered, disposition,
q850_cause, hangup_originator, completed — see
dispositions.
Leg — media quality
Section titled “Leg — media quality”Sampled once at hangup from RTP/RTCP; zero/absent for legs with no media or very short calls:
| Field | Meaning |
|---|---|
codec |
Negotiated audio codec |
rtp_rx_loss / rtp_tx_loss |
Packets lost, each direction |
rtp_rx_jitter / rtp_tx_jitter |
Jitter (ms) |
rtp_rtt |
Round-trip time (ms) |
rtp_rx_mes / rtp_tx_mes |
Media experience score, each direction |
rtp_rx_count / rtp_tx_count |
Packets received/sent |
rtp_stats |
The raw RTCP dump, kept as a forensic backstop |
Leg — enrichment
Section titled “Leg — enrichment”| Field | Meaning |
|---|---|
number_info |
JSON jurisdiction data (OCN, LATA, rate center, state, country, call type) for the calling/called/routing numbers — present when a translation modify block sets collect-numberinfo |
cdr_schema_version |
Record-format version stamp |
Retention
Section titled “Retention”CDRs are kept indefinitely — there is no automatic expiry. Retention policy is yours to enforce, with the range purge described in Managing CDRs.