Skip to main content

CallDetailRecord Model

The CallDetailRecord model stores the final record of every call, created from the Cloudonix CDR webhook. Records age through three storage tiers.

Table: call_detail_records (hot), archived_call_detail_records (warm)
Models: App\Models\CallDetailRecord, App\Models\ArchivedCallDetailRecord

Database Schema​

ColumnTypeNullableDescription
idbigint unsignedNoPrimary key (kept when a row moves to the warm table)
organization_idbigint unsignedNoTenant scope
session_timestamptimestampNoSession time. Tiering is based on this, in UTC
session_tokenvarchar(100)YesCloudonix session token
from / tovarchar(100)NoCaller ID (display only) and called number (destination)
extension_numbervarchar(20)YesThe organization's extension the call belongs to, set at ingest by extensionNumberFor(). Null when no rule applies
directionvarchar(20)Yesincoming, outgoing, internal or application. Copied from the latest session update when the CDR is ingested; null for older rows
dispositionvarchar(50)NoFinal disposition (for example ANSWER, BUSY)
duration / billsecintNoTotal and billed seconds. Default 0
call_idvarchar(255)NoCloudonix call ID
domainvarchar(255)YesCloudonix domain
subscribervarchar(100)YesSubscriber (extension) UUID
cx_trunk_idintYesTrunk
application, routevarchar(255)YesCloudonix routing details
vapp_servervarchar(50)YesCloudonix voice application server
rated_cost, approx_cost, sell_costdecimal(10,4)YesCosts
session_idbigintYesCloudonix session ID
call_start_time, call_answer_time, call_end_timetimestampYesCall times
statusvarchar(50)YesStatus
recording_statusenumNonone (default), pending, available, failed
recording_source_url, recording_stored_path, recording_mime_typevarchar(255)YesRecording location
recording_durationintYesRecording length
amd_resultvarchar(20)YesAMD result for auto-dialer calls
amd_confidencedecimal(5,2)YesAMD confidence
raw_cdrjsonNoThe CDR payload as received
is_auto_dialerbooleanNoAuto-dialer call. Default false
auto_dialer_campaign_idbigint unsignedYesCampaign. Foreign key to auto_dialer_campaigns, ON DELETE SET NULL
created_at / updated_attimestampYes

archived_call_detail_records has the same columns and keeps the original IDs.

organization_id references organizations with ON DELETE CASCADE. amd_result, amd_confidence, is_auto_dialer and auto_dialer_campaign_id are not mass assignable; they are set separately for auto-dialer calls.

Indexes​

Single-column indexes on call_id, disposition, domain, from, to, session_timestamp, session_token and auto_dialer_campaign_id, plus these composite indexes:

IndexColumns
cdr_org_ts_idxorganization_id, session_timestamp
cdr_org_disp_idxorganization_id, disposition
cdr_org_from_idxorganization_id, from
cdr_org_to_idxorganization_id, to
cdr_org_ext_idxorganization_id, extension_number (warm table: acdr_org_ext_idx)
cdr_org_session_idxorganization_id, session_id
cdr_auto_dialer_org_idxis_auto_dialer, organization_id

Storage Tiers​

TierWhereAge (default)
Hotcall_detail_records in the main databaseUp to CDR_HOT_MONTHS (6)
Warmarchived_call_detail_records on the cdr_archive connection. That is the main schema unless DB_CDR_ARCHIVE_DATABASE (and the optional DB_CDR_ARCHIVE_HOST, _PORT, _USERNAME, _PASSWORD) points elsewhereUp to CDR_WARM_MONTHS (12)
ColdCompressed CSV files in the CDR_ARCHIVE_BUCKET bucket (default cdr-archive)Kept CDR_COLD_RETENTION_YEARS (7)

cdr:tier moves rows between tiers daily at 04:00 UTC, and recordings:expire deletes expired recordings at 04:30 UTC (RECORDING_RETENTION_MONTHS, at most the cold retention). Call records are never pruned by model:prune. ArchivedCallDetailRecord is read-only and is written only by cdr:tier. See Call Logs.

Relationships​

$cdr->organization() → Organization
$cdr->extension() → Extension (by subscriber = cloudonix_uuid)
$cdr->sessionUpdate() → SessionUpdate (latest for the session)

Methods​

CallDetailRecord::createFromWebhook(array $payload, int $organizationId): self
CallDetailRecord::extensionNumberFor(int $organizationId, ?string $subscriber, string $from, string $to): ?string
CallDetailRecord::forOrganization($orgId)
CallDetailRecord::withDisposition($disposition)
CallDetailRecord::connected()
CallDetailRecord::betweenDates($start, $end)
$cdr->formatted_duration / $cdr->formatted_billsec / $cdr->amd_status

createFromWebhook() takes a cache lock on cdr:create:{organization_id}:{call_id} (held up to 10 seconds, waits up to 5) and returns the existing row for that call_id if there is one, so a redelivered CDR does not create a second row. extensionNumberFor() returns, within the organization, the extension whose cloudonix_uuid is the CDR's subscriber (the subscriber that placed the call), else the extension whose number is the caller ID without + (API-initiated calls), else the extension that the DID in to (with or without +) routes to directly (routing_type extension), else the answering extension OPBX recorded for the CDR's session_token; otherwise null. recordAnsweringExtension($organizationId, $sessionToken, $extensionNumber) stores that answering extension in Redis (cdr:answerer:{organization_id}:{session_token}, SET NX with a 24-hour TTL, so the first one recorded wins) and sets extension_number on an already stored CDR of that session whose extension_number is still null. It is called by UserRoutingStrategy (any single-extension dial: DID, IVR option, business hours, fallbacks), by the ring group callback when a Sequential or Round Robin member's dial is answered (DialCallStatus/CallStatus answered, completed or in-progress; the member travels in the callback URL as dialed_extension), by RingGroupConferenceService::claim when a member of a Simultaneous ring group answers its own call (the member's extension number, keyed by the caller's session token), and by queue finalization (QueueCallLifecycleService::finalizeFromCdr, the agent's extension for an answered call). Ring All queue dials with separate calls switched off (one multi-number <Dial>) record nothing: Cloudonix does not report the winning leg. The migration 2026_10_14_220000_add_extension_number_to_call_detail_records added the column on both tiers and filled it for existing rows with the same rule (idempotent: only rows still null are touched). The migration 2026_10_15_090000_backfill_cdr_extension_number_from_queue_agents fills, on both tiers, CDRs still null whose session_token matches an answered queue_calls row (by session_token or call_id) with the extension of its agent_user_id (idempotent). extension() matches subscriber against extensions.cloudonix_uuid; sessionUpdate() is the latest session_updates row with the same session_id.