Skip to content

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.

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.

  • 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.

CDR rows are written open and finalized in place — a record exists from the moment the call does, not only after it ends:

  1. Call start — the header row is written with completed: false.
  2. 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.
  3. Answer — the leg row is updated with answer_start, billing_start, and the negotiated codec.
  4. 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, and completed flips to true.
  5. 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.

Field Measures
event_startevent_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_startevent_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.

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.

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-out in 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_start resets to the transfer instant, so the two segments’ billable_ms add up to the real total without double-billing.
  • The two headers point at each other: linked_ulid carries the counterpart’s ULID and link_reason is transfer.

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 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
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, …)
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
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

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.

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
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

CDRs are kept indefinitely — there is no automatic expiry. Retention policy is yours to enforce, with the range purge described in Managing CDRs.