Call Detail Records (CDR) Reporting

Live CDR Reporting

 

The CDR URL deals with the problem that the PBX needs to notify external servers about finished call legs. This is slightly different from the problem than informing about calls, as calls may go through different states. For example, when a call comes in on a trunk, goes to an auto attendant, then to a ACD, gets picked up by an agent, then is transferred without consultation to another agent, there is only one trunk CDR being written regardless of the various transitions that the whole call went through. This can be seen as the carriers perspective, who does not care much about what exactly happened inside the PBX; the only interest is to find out when the call started, when it got connected and when it got disconnected. The call records in the web interface of the system and the domain are different from that, as they display the whole call regardless of the call legs that were used inside the call object.

The CDR URL contains zero, one or more strings that tell the PBX where to send leg CDR. These strings are separated by space characters.

The URL must start with a scheme, then followed by a colon. The following schemes are supported:

Scheme Format
webcdr The CDR is sent by the HTTP protocol with a JSON attachment.
mongodb The CDR is sent to a MongoDB database server.
json, jsons The CDR is sent by the HTTP protocol with a JSON attachment.
mailto The CDR is send by email.
file, filet, fileto, fileti The system appends a record to a file.
cdr, trunk, trunkout, trunkin The system sends a CDR line over a TCP connection.
http, https, soap, soaps The CDR is sent by the HTTP protocol with a XML attachment.

MongoDB

MongoDB is a powerful database that can store BSON objects (see mongodb.com). The PBX can write CDR records natively into the database. The schema for the URL is like this: “mongodb://server:port/database/table”. The server must be an IP address or “localhost” for the loopback address “127.0.0.1”. The port defaults to 27019 which is currently the standard MongoDB port; however we recommend to explicitly specify the port. The database string tells the server where to store the CDR; this string may contain the variable “{domain}” which is replaced with the name of the domain. The table name is a string that tells the PBX where to store the CDR.

The feature is available from version 5.3 for the hosted PBX.

The CDR is a BSON object that contains general information about the call, an array of extension-related records, of trunk-related records, of internal state-related information and about recordings in this call. The following tables show the details of the structures.

General Information

For each call the system generates one record in the database. It has the following structure.

Field Meaning
domain The primary (canonical) name of the domain in which the call took place.
callid The primary Call-ID of the call. This is typically the Call-ID of the first call leg of the call. However during transfer and pickup scenarios, this can be also the Call-ID of another call leg.
from The “From” field of the call. This is typically the one who started the call. The format is like it appears in the SIP headers, including the display name and the URI for the call. This field is mainly for descriptive purposes and should not be used to make assumptions on billing-related topics.
to The “To” field of the call, similar to the “From” field. This field indicates the other party of the call.
start The timestamp when the call was created.
connect The timestamp when the call was “connected” the first time. The call can get connected again later, for example when users use the PBX to make outbound calls from outside telephones.
end The timestamp when the last call leg was disconnected.
recordings An array describing the recordings in this call (see below).
trunklegs An array describing the trunk legs in this call (see below). Legs are trunk legs when there was a trunk involved in the call.
extensionlegs An array describing the extension legs in this call (see below).
states An array describing the states in this call (see below).

Extension Records

Legs are extension legs if the call was connected to an extension of the PBX. This is typically the case when the call was connected to a VoIP phone of WebRTC client in the user portal. Calls that are associated with an extensions cell phone are both trunk and extension legs and will have an entry in trunklegs and extensionlegs. In this case, the Call-ID of the legs will be the same.

Field Meaning
callid The Call-ID for this leg.
from The value of the “From” header at the time when the call leg started. Note that the value may change afterwards.
to The value of the “To” header, like the value of the “From” header but the other direction of the call.
direction The direction of the call. The value of this field can be either “I” for inbound calls or “O” for outbound. The direction depends on the users experience for this call leg. For example in a transfer scenario where another user starts an outbound call and then transfers that call to an user, that user would see this as an inbound call.
extension The primary account name of the extension.
redirect The value of the “To” header as it was presented to the users device. This might be different from the “To” header, for example because of settings in the group on how caller-ID should be presented.
idle The number of seconds between hanging up the last call before picking up this call leg for this extension. If the duration is longer than the maximum idle time of the domain, the value is not reported; this usually means that the agent came back in the morning.
start A timestamp when that call leg was started.
connect The timestamp when that call leg was connected. If the call was not connected (e.g. only ringing), then this timestamp is not present.
end The timestamp when that call leg was disconnected.
ipadr The IP address that was perceived from the PBX for this call.
quality The quality report that was calculated by the PBX for this call leg (see RFC 6035 for more information).
type The call type for this call leg: “m” means missed call, “r” means received (connected) call and “d” means dialled, outbound call.
cmc If there was a client matter code set for this call leg, it will be reported in this field.
codec The codec was was used for this call.

Trunk Records

Field Meaning
direction The direction of the call , “I” for incoming and “O” for outgoing for this specific call, independent from the underlying logic of the call. For example, when forking an inbound call to the PBX to a user’s cell phone, the cell phone call leg will appear as outbound call.
from The value of the “From” header at the time when the call leg started. Note that the value may change afterwards.
to The value of the “To” header, like the value of the “From” header but the other direction of the call.
remoteparty The remote party of the call. For outbound calls, it contains the number that is used for billing. This is often similar to the “To” header, but can differ for example when group calls are triggering trunk calls.
localparty The local party of the call.
trunk The name of the trunk was was used for the call.
cost The cost for the call, when available. Cost can be defined in the rates table for the trunk.
start A timestamp when that call leg was started.
connect The timestamp when that call leg was connected. If the call was not connected (e.g. only ringing), then this timestamp is not present.
end The timestamp when that call leg was disconnected.
ipadr The IP address that was perceived from the PBX for this call.
quality The quality report that was calculated by the PBX for this call leg (see RFC 6035 for more information).
cmc If there was a client matter code set for this call leg, it will be reported in this field.
extension If this call was associated with an extension, for example because this was a call to a user’s cell phone, the primary name of the extension is reported here.
codec The codec was was used for this call.
mos The calculated MOS score for the call as reported in the MOS score table of the trunk.

Internal State

Calls are going through one or more states. For example, a call might first hit an auto attendant, then gets transferred into a hunt group, then after a timeout into a IVR node and then into a mailbox. The states array shows the history of those states.

Field Meaning
type The type of the call. This field can take the following values:

  • acd: When the call goes to a agent group (ACD).
  • attendant: When the call goes to an auto attendant this state is used. Also, when someone calls an extension directly (not through a group), this state is used.
  • mailbox: When a call hits the mailbox, it uses this state.
  • extcall: When a user places a regular external call, this state is used. It may also contain special operations, like entering a PIN code for authentication purposes.
  • hunt: This state is used for hunt group calls.
  • conference: In a conference call, this state is used.
  • orbit: When the call is in a park orbit, it uses the orbit state.
  • hoot: The type hoot is used during paging.
  • srvflag: This state means that the service flag was called (in order to change it).
  • ivrnode: This state is used when processing IVR nodes.
  • caller: This state is used when the PBX initiates calls, for example when inviting participants for conferences or waking up hotel guests.
  • starcode: This state is used when processing starcodes, e.g. DND or call redirection.
from The value of the “From” header at the time when the call leg started. Note that the value may change afterwards.
to The value of the “To” header, like the value of the “From” header but the other direction of the call.
language The language that was used in this state. The language is a two-digit code, e.g. “en” for US-English and “de” for German.
start A timestamp when that state was entered.
durationivr The duration in ms how long the call was in a IVR state, rendering annoucements to the user.
durationring The duration how long the call was ringing a user.
durationtalk The duration how long the call was connected to a user. This is independent from the trunk connected time. It also includes the time when the call was on hold.
durationhold The duration how long the call was on hold in this state.
durationidle The duration how long the extension was idle (see idle above), if there was a extension connected in the state.
reason The reason for terminating the state. This field can have the following values:

  • hc: The call was connected in the ACD and cleared normal.
  • hw: The call was disconnected by the user while waiting in the ACD.
  • hr: The call was disconnected by the user while ringing in the ACD.
  • hb: The call was disconnected by the user while the ACD was sending a busy signal.
  • rw: The state was terminated because of wait timeout in the ACD.
  • rr: The state ended because the call was ringing too long in the ACD.
  • ra: The call was redirected because it was an anonymous call in the ACD.
  • user: The user pressed a DTMF key that triggered an action in the ACD.
  • soap: A SOAP response triggered a redirection or termination in the ACD.
  • missed: The call was missed in the auto attendant.
account The account that was used for the state. This is primary name of the account that was used in the state.
extension If there was a extension involved in the state, this field contains the name of the extension. This is the connected agent in the case of a ACD or hunt group.

Recording Records

During a call, zero, one or more recordings can be triggered. For example because the trunk setting triggered a recording, or because the ACD was set to record the call. Recordings can cover a part of the call or the whole call, depending on who triggered the recording.

Field Meaning
time The timestamp for when the recording started.
file The location of the file in the file system.
account The account that triggered the recording.
extension The extension that was involved in the recording.

webcdr

JSON has become a popular data format because it can be easily parsed in JavaScript and many other script language. The PBX generates an object that contains a number of variables. This method is gives a JSON cdr that is easier to parse and make sense of than the previous json method described below, which is still supported for backward compatibility.

URL

The URL for sending out JSON CDR consists of a scheme which should be webcdr in this case, the server address, an optional port number and an optional resource.

The PBX will send a CDR record that contains relevant information for the call, inclduing a field for trunklegs and another one for extensionlegs for extra leg details. Because the CDR will be processed by a script anyway, that script can easily take care about filtering the relevacnt records out. For example, the script can check if the record contains a trunk and is outbound, and if not discard the record.

The resource can be used to point the server to the right file, typical examples are “/cdr” or “/json/cdr”. If not provided the resource will default to “/”.

Format

The JSON object consists of several entries:

system Text The activation code for the system. This code can be used to identify the PBX system.
domain Text The domain for the call object.
callid Text This is the Call-ID for the call. This Call-ID should be unique.
from Text The originator of the call. The format is according to the SIP standard with an optional display name and a SIP URI. The real destination may be different. This field shows what the original call headers were when the call was set up.
to Text The destination of the call. The format is the same like the From header.
start Date/Time The time when the call was started in GMT.
connect Date/Time The time when the call was connected in GMT. This field may be empty if the call was not connected.
end Date/Time The time when the call ended in GMT.
recordings JSON Object If the call was recorded automatically, this field contains the location where the call was recorded.
trunklegs JSON Array Details of the trunk legs as a json array.
extensionlegs JSON Array Details of the extension legs as a json array.

PHP Example Code

The following code can be used as an example for storing the CDR in a PHP/MySQL environment.

<?php
// Check where this request is coming from:
if (!isset($_SERVER['PHP_AUTH_USER'])) {
  header('WWW-Authenticate: Basic realm="CDR Reporting"');
  header('HTTP/1.0 401 Unauthorized');
  echo 'Please enter a username and a password';
  exit;
}


// Connect to the database:
$report_host = "localhost";
$report_user = "root";
$report_pass = "pbxnsip";
$report_name = "cdr";

$mysqli = new mysqli($report_host, $report_user, $report_pass, $report_name);
if($mysqli->connect_errno) exit("Connect failed: " . $mysqli->connect_error);

// Check if the user exists in the database:
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
$result = $mysqli->query("SELECT id FROM system WHERE username='" .
                         $mysqli->real_escape_string($username) .
                         "' AND password='" .
                         $mysqli->real_escape_string($password) .
                         "'");
if($result == FALSE || $result->num_rows != 1) exit("User not found or password");
$row = $result->fetch_assoc();
$system = $row["id"];

// Parse a SIP address
// "UTF-8" <sip:number@domain>
// UTF-8 <sip:number@domain>
// <sip:number@domain>
// sip:number@domain
function parse_sip_address($adr, &$name, &$number) {
  // Try if this contains a name:
  if(preg_match('/"(.*)" *<sip:(.*)@.*>/', $adr, $matches) > 0) {
    $name = $matches[1];
    $number = $matches[2];
    return;
  }
  if(preg_match('/(.*) *<sip:(.*)@.*>/', $adr, $matches) > 0) {
    $name = $matches[1];
    $number = $matches[2];
    return;
  }
  if(preg_match('<sip:(.*)@.*>/', $adr, $matches) > 0) {
    $name = "";
    $number = $matches[1];
    return;
  }
  if(preg_match('sip:(.*)@.*/', $adr, $matches) > 0) {
    $name = "";
    $number = $matches[1];
    return;
  }

  // Can't figure it out:
  $name = "";
  $number = "";
}

// Take a number
function parse_date($date) {
  return date('Y-m-d H:i:s', $date);
}

// Check if this comes in JSON form:
if(isset($_SERVER["CONTENT_TYPE"]) && $_SERVER["CONTENT_TYPE"] == "application/json") {
  $json = json_decode(file_get_contents("php://input"));

  parse_sip_address($json->from, $fname, $fnumber);
  parse_sip_address($json->to, $tname, $tnumber);

  // Add the row:
  $query = sprintf("INSERT INTO cdr (system,callid,fnumber,tnumber,fname,tname,start,connect,end) values ('%s','%s','%s','%s','%s','%s','%s','%s','%s')",
                   $mysqli->real_escape_string($system),
                   $mysqli->real_escape_string($json->callid),
                   $mysqli->real_escape_string($fnumber),
                   $mysqli->real_escape_string($tnumber),
                   $mysqli->real_escape_string($fname),
                   $mysqli->real_escape_string($tname),
                   $mysqli->real_escape_string(parse_date($json->start)),
                   $mysqli->real_escape_string(parse_date($json->connect)),
                   $mysqli->real_escape_string(parse_date($json->end)));
  $mysqli->query($query);
  $id = $mysqli->insert_id;

  $reqrec = array();
  for($i = 0; $i < count($json->recordings); $i++) {
    $rec = $json->recordings[$i];
    $filename = sprintf("rec-%d-%d.wav", $id, $i + 1);
    $reqrec[$filename] = $rec->file;
    $query = sprintf("INSERT INTO rec (cdr,start,file,account,extension) values ('%d','%s','%s','%s','%s')",
                   $id,
                   $mysqli->real_escape_string(parse_date($rec->time)),
                   $mysqli->real_escape_string($filename),
                   $mysqli->real_escape_string($rec->account),
                   $mysqli->real_escape_string($rec->extension));
    $mysqli->query($query);
  }

  // Tell the PBX which recorings need to be uploaded:
  $myadr = sprintf("%s://%s:%d%s", $_SERVER['HTTPS'] ? "https" : "http", $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['REQUEST_URI']);
  $json = '{"upload":[';
  $first = TRUE;
  foreach($reqrec as $i => $v) {
    if(!$first) { $json .= ','; $first = FALSE; }
    $json .= sprintf('{"file": "%s", "url": "%s/%s"}', $v, $myadr, $i);
  }
  $json .= ']}';
  echo $json;
}

// Here you can set the table up. Make sure that you CHANGE THE PASSWORD.
else if(isset($_GET["action"]) && $_GET["action"] == "setup" && $_GET["password"] == "secret") {
}

?>

json, jsons

JSON has become a popular data format because it can be easily parsed in JavaScript and many other script language. The PBX generates an object that contains a number of variables. The variable “action” is set in that object to “call-data-record”, so that the receiving end can easily figure out that this is a CDR record.

JSON CDR are supported in version 5.0.8 and higher.

URL

The URL for sending out JSON CDR consists of a scheme, the server address, an optional port number and an optional resource. The URL is similar to HTTP URL known from browsing the internet. The scheme can be either “json” or “jsons”. “json” uses HTTP, while “jsons” uses HTTPS transport layer.

The PBX will send a CDR record for all call legs. Because the CDR will be processed by a script anyway, that script can easily take care about filtering the relevacnt records out. For example, the script can check if the record contains a trunk and is outbound, and if not discard the record.

The server address can be either an IP address or a DNS address. IP addresses can be IPv4 addresses (e.g. “192.168.1.2”) or IPv6 addresses (e.g. “[db8:32::4f3:435]”). DNS addresses must have either DNA A or DNS AAAA records available. If the port number is not provided, it defaults to 80 for HTTP and 443 for HTTPS.

The resource can be used to point the server to the right file, typical examples are “/cdr” or “/json/cdr”. If not provided the resource will default to “/”.

Format

The JSON object consists of several entries:

Entry Type Description
action Text This entry has always the value “call-data-record”
System Text The activation code for the system. This code can be used to identify the PBX system.
PrimaryCallID Text The Call-ID for the call object. There may be several legs that reference this Call-ID. This is a way to link them together.
CallID Text This is the Call-ID for the call leg. This Call-ID should be unique.
From Text The originator of the call. The format is according to the SIP standard with an optional display name and a SIP URI. The real destination may be different. This field shows what the original call headers were when the call was set up.
To Text The destination of the call. The format is the same like the From header.
Direction Text The direction of the call. The direction can be “I” (inbound) or “O” (outbound).
RemoteParty Text The remote party for this call leg from the PBX point of view. This format for this field is the number only (not in SIP header style).
LocalParty Text The local party for this call leg from the PBX point of view; usually an extension number. This format for this field is the number only (not in SIP header style).
TrunkName Text The name of the trunk that was used for this call, if this call leg was on a trunk.
TrunkID Integer The number of the trunk on the system. While trunk names don’t necessarily have to be unique, the TrunkID is.
Cost Text The cost for this call, if available. In order to use this element, the trunk must have a rate table set up.
CMC Text The client matter code (customer number) for the call, if available.
Domain Text The name of the domain in which the call was made.
TimeStart Date/Time The time when the call was started in GMT.
TimeConnected Date/Time The time when the call was connected in GMT. This field may be empty if the call was not connected.
TimeEnd Date/Time The time when the call ended in GMT.
LocalTime Date/Time The local time of the domain when the call was connected.
DurationHHMMSS Text Duration of the call in the format HHMMSS (hour, minute, second).
Duration INT The duration of the call in seconds.
RecordLocation Text If the call was recorded automatically, this field contains the location where the call was recorded.
Type Text The type of the call. For example, when the call went to voicemail, the type is “mailbox”.
Extension Text If this call leg was connected to an extension, this field contains the canonical name of the extension. Note that for calls that went to a user’s cell phone, both the extension and the trunk information will be available.
IdleDuration INT The duration how long the extension was idle before connecting the call.
RingDuration INT The duration how long the call was in ringing state before connecting the call.
HoldDuration INT The duration how long the call was on hold.
IvrDuration INT The duration how long the call was in IVR state before connecting the call. This entry is important in ACD calls, where callers are waiting before the call starts ringing.
AccountNumber Text The accounts number for the call, if available. The meaning of the field depends on the type of the call. For example, for ACD calls this field will contain the number of the ACD.
IPAdr Text The IP address of the remote site.
Quality Text The call quality summary for the call.

PHP Example Code

The following code can be used as an example for storing the CDR in a PHP/MySQL environment.

<?php

// That needs to be set up accordingly:
$report_host = "localhost";
$report_name = "cdrdb";
$report_pass = "password";
$report_user = "root";

// Check if this comes in JSON form:
if(isset($_SERVER["CONTENT_TYPE"]) && $_SERVER["CONTENT_TYPE"] == "application/json") {
  $mysqli = new mysqli($report_host, $report_user, $report_pass, $report_name);
  if($mysqli->connect_errno) exit("Connect failed: " . $mysqli->connect_error);

  $json = json_decode(file_get_contents("php://input"));
  if($json->action == "call-data-record") {
    // Check if the request comes from the expected IP address:
    $system = $mysqli->real_escape_string($json->System);
    $remote = $_SERVER["REMOTE_ADDR"];
    $result = $mysqli->query("SELECT * FROM system WHERE code='$system' AND adr='$remote'");
    if($result == FALSE || $result->num_rows != 1) exit("System not found or request from wrong IP address");

    // Add the row:
    $query = sprintf("INSERT INTO cdr (sys,primarycallid,callid,cid_from,cid_to,direction,remoteparty,localparty,trunkname,trunkid,cost,cmc,domain,timestart,timeconnected,timeend,ltime,durationhhmmss,duration,recordlocation,type,extension,idleduration,ringduration,holdduration,ivrduration,accountnumber,ipadr) values ('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')",
        $mysqli->real_escape_string($json->System),
        $mysqli->real_escape_string($json->PrimaryCallID),
        $mysqli->real_escape_string($json->CallID),
        $mysqli->real_escape_string($json->From),
        $mysqli->real_escape_string($json->To),
        $mysqli->real_escape_string($json->Direction),
        $mysqli->real_escape_string($json->RemoteParty),
        $mysqli->real_escape_string($json->LocalParty),
        $mysqli->real_escape_string($json->TrunkName),
        $mysqli->real_escape_string($json->TrunkID),
        $mysqli->real_escape_string($json->Cost),
        $mysqli->real_escape_string($json->CMC),
        $mysqli->real_escape_string($json->Domain),
        $mysqli->real_escape_string($json->TimeStart),
        $mysqli->real_escape_string($json->TimeConnected),
        $mysqli->real_escape_string($json->TimeEnd),
        $mysqli->real_escape_string($json->LocalTime),
        $mysqli->real_escape_string($json->DurationHHMMSS),
        $mysqli->real_escape_string($json->Duration),
        $mysqli->real_escape_string($json->RecordLocation),
        $mysqli->real_escape_string($json->Type),
        $mysqli->real_escape_string($json->Extension),
        $mysqli->real_escape_string($json->IdleDuration),
        $mysqli->real_escape_string($json->RingDuration),
        $mysqli->real_escape_string($json->HoldDuration),
        $mysqli->real_escape_string($json->IvrDuration),
        $mysqli->real_escape_string($json->AccountNumbery),
        $mysqli->real_escape_string($json->IPAdr));
    $mysqli->query($query);
  }
}

// Here is some code to set the table up. Make sure that you CHANGE THE PASSWORD.
else if(isset($_GET["action"]) && $_GET["action"] == "setup" && $_GET["password"] == "secret") {
  $mysqli = new mysqli($report_host, $report_user, $report_pass, $report_name);
  if($mysqli->connect_errno) exit("Connect failed: " . $mysqli->connect_error);
  $query = sprintf("CREATE TABLE cdr (" .
    "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY," .
    "sys VARCHAR(16)," .
    "primarycallid VARCHAR(255)," .
    "callid VARCHAR(255)," .
    "cid_from VARCHAR(255)," .
    "cid_to VARCHAR(255)," .
    "direction VARCHAR(4)," . // I or O
    "remoteparty VARCHAR(255)," .
    "localparty VARCHAR(255)," .
    "trunkname VARCHAR(255)," .
    "trunkid INT," .
    "cost VARCHAR(10)," .
    "cmc VARCHAR(20)," .
    "domain VARCHAR(255)," .
    "timestart DATETIME," .
    "timeconnected DATETIME," .
    "timeend DATETIME," .
    "ltime DATETIME," .
    "durationhhmmss VARCHAR(10)," .
    "duration INT," .
    "recordlocation VARCHAR(255)," .
    "type VARCHAR(10)," .
    "extension VARCHAR(255)," .
    "idleduration INT," .
    "ringduration INT," .
    "holdduration INT," .
    "ivrduration INT," .
    "accountnumber VARCHAR(20)," . // Assuming extensions are never longer than 20 characters
    "ipadr VARCHAR(40))"); // Enough space for IPv6
  $mysqli->query($query);
  printf($query);
}
?>

mailto

CDR can also be send by email. Sending out emails has the advantage that the sending procedure is very reliable, even if the system should be disconnected to the outside world for a longer time. Also, archiving emails is easy, especially when email accounts are available for free with a lot of available space. Because the CDR URL string may contain several destinations, this may help as a secondary backup CDR storage.

The email will contain the values from the other CDR record types, listed in a line-based format. The fields are From, To, Primary-CID, Call-ID, Direction, Type, Remote, Local, TrunkName, TrunkID, Domain, LocalTime, Start, Connect, DurationHHMMSS, DurationSec, End, AccountNumber, Cost, IPAdr, RecordLocation, RecordUsers and CMC.

file, filet, fileto, fileti

Writing CDR to a file on the file system is a simple, common way to keep track on the activity of the PBX.

Scheme Meaning
file All CDR are written. This includes CDR for internal calls. A CDR is written for each call leg, resulting in multiple CDR for each call.
filet A record is written only if a trunk is involved.
fileto A record is written only if a trunk is involved and the call was outbound on that trunk.
fileti A record is written only if a trunk is involved and the call was inbound on that trunk.

The location of the CDR can be set with the value behind the scheme. The following patterns are automatically expanded before opening the file:

Short Description
$$ A dollar sign
$c Replaced with the value “cdr” (legacy).
$m The name of the domain. This makes it easy to keep different files for each domain.
$d The date in the 8-digit format YYYYMMDD.
$h The hour in 2-digit format HH.

The system automatically appends for each CDR a new line to the resulting file. The line contains the following information:

  • The domain name
  • The primary call-ID of the call
  • The leg call-ID
  • The name of the one who started the call (From)
  • The name of the one who received the call (To)
  • The direction of the call
  • The type of the call, for example attendant or hunt
  • The start timestamp in the format YYYYMMDDHHMMSS
  • The timestamp when the call got connected in the format YYYYMMDDHHMMSS, if the call leg was connected at all
  • The timestamp when the call got disconnected in the format YYYYMMDDHHMMSS
  • The name of the trunk
  • The local party
  • The remote party
  • The recording location
  • The client matter code, if present
  • The IP address of the connected party
  • The call quality report, if it has been enabled
  • The extension associated with the call, for example the agent in the ACD group
  • The account that was in use (e.g. auto attendant or hunt group) when the call leg ended

cdr, trunk, trunkout, trunkin

There are several commercial tools available that collect CDR information through simple TCP-based communication. Typically these tools emulate the behavior of old RS-232 based systems in a network world.

The different schemes are filtering the content that is being sent to the CDR recipient.

Scheme Meaning
cdr All CDR are sent. This includes CDR for internal calls. A CDR is sent for each call leg, resulting in multiple CDR for each call.
trunk A record is sent only if a trunk is involved.
trunkout A record is sent only if a trunk is involved and the call was outbound on that trunk.
trunkin A record is sent only if a trunk is involved and the call was inbound on that trunk.

Each CDR is sent in one line of ASCII text terminated by a CRLF pair over a TCP-based communication link. In order to send the simple CDR to an external server, you need to specify the IP address and the port for the server. You do this in the SOAP CDR setting on the admin level of the PBX. In order to differentiate the destination from a SOAP CDR, you must use one of the schemes in front of the IP address and the port. The fields are separated by a colon. For example, “cdr:192.168.1.2:10000” would send the CDR to the IP address 192.168.1.2 on port 10000.

You can define a format string that the PBX uses to generate the CDR line. You can use any characters in this string; however the dollar sign has a special meaning. The format can be defined in the global setting with the name “Format template for simple CDR strings”. When sending plain text CDR from the PBX, you can use the following fields:

Short Long Description Example
$$ $$ A dollar sign $
$i $(call_id) Caller-ID of the call leg, as seen in the SIP packet. Note that a call usually has several legs with different call-ID on their legs.
$v $(call_type) Type of call at the time when the call leg ended. attendant
$m $(domain) Domain name of the call leg. domain.com
$l $(lang) Language of the call at the time when the call-leg was sent. en
$f $(sip_from) The From header, as seen in SIP packet. “Alice” <sip:123@domain.com;user=phone>
$t $(sip_to) The To header, as seen in the SIP packet. “Bert” <sip:456@domain.com;user=phone>
$F $(calling) The phone number of the caller, taken the From header. 123
$T $(called) The phone number of the caller, taken the To header. 456
$I $(ivr_user) The account that was used for the call when the leg CDR was generated. Typically this is an auto attendant, hunt or ACD group. 73
$x $(orig_trunk) Name of trunk of the call leg Trunk 1
$R $(account) Account that is charged for redirected call, including the domain name 45@localhost
$g $(charge) Account that is charged for redirected call, without the domain name 45
$r $(redirect_dest) Destination for a redirected call (used only if call is redirected)
$S $(start_time) Start time in seconds since 1970
$C $(talk_duration) Connected duration of the call
$A $(ring_duration) Answered duration of call (useful if the call is ringing/answered through queue)
$E $(hold_duration) Hold duration for the call
$W $(wait_duration) IVR duration for the call
$w $(start_date_time) Start time (YYYYMMDDHHMMSS) 20071023123224
$b $(date) Date (YYYYMMDD) 20071023
$B $(time) Time (HHMMSS)
$e $(extension) Extension number of the call leg. Note that this is not necessarily the extension that was charged for the call (see g parameter). 45
$o $(direction) Direction of call (i for inbound; o for out) i or o
$c $(remote_call_id) Caller-ID of remote party
$d $(duration) Duration of call in seconds (include hold time)
$s $(extn_duration) Speaking duration (does not include hold time)
$M $(cmc) Client matter code (CMC) 781299
$(record_location) The location of the recorded files for the call leg, if present.
$(primary_call_id) The primary call-ID for the call. Several call legs that are part of the same call with have the same primary call-ID.

You can put the length of the string between the dollar sign and the character. For example the expression “$10c” will insert a 10-digit caller-ID into the string. An example would look like “$w$5g$12c$5d”. It would generate line that could look like this: “20071223123224 123 9781234567 120”.

http, https, soap, soaps

The PBX also supports sending CDR information in XML format. This feature was mainly kept for backward compatibvilty. For new implementations we recommend to use the JSON-based format instead. The data interchange is done by using the HTTP/SOAP protocol. The CDR is encoded in a XML format and looks like this. The real file will not have indentation; here it is only for illustration purposes.

POST /cdr.xml HTTP/1.1
Host: pbx.com
SOAPAction: CDR
Content-Type: text/xml
Content-Length: 123

<env:Envelope 
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:sns="http://www..com/soap/pbx">
  <env:Body>
    <sns:CDR>
      <From>Fred Feuerstein <sip:ff@test.com></From>
      <To>Tom Test <sip:tt@test.com></To>
      <Type>extcall</Type>
      <TimeStart>464645649</TimeStart>
      <TimeConnected>464645655</TimeConnected>
      <TimeEnd>464645676</TimeEnd>
      <CallID>8c72b11bcd4@192.168.2.44</CallID>
      <Domain>test.com</Domain>
    </sns:CDR>
  </env:Body>
</env:Envelope>
<pre>

The XML schema for the CDR is defined by the following XML:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns="http://tempuri.org/XMLSchema.xsd" elementFormDefault="qualified" targetNamespace="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
   <xs:element name="CDR">
       <xs:complexType>
           <xs:attribute name="CallID" type="xs:string" />
           <xs:attribute name="Type">
               <xs:simpleType>
                   <xs:restriction base="xs:string">
                       <xs:enumeration value="attendant" />
                       <xs:enumeration value="conference" />
                       <xs:enumeration value="extension" />
                       <xs:enumeration value="ivrnode" />
                       <xs:enumeration value="mailbox" />
                       <xs:enumeration value="acd" />
                       <xs:enumeration value="extcall" />
                       <xs:enumeration value="starcode" />
                   </xs:restriction>
               </xs:simpleType>
           </xs:attribute>
           <xs:attribute name="Domain" type="xs:string" />
           <xs:attribute default="en" name="language">
               <xs:simpleType>
                   <xs:restriction base="xs:string">
                       <xs:enumeration value="en" />
                       <xs:enumeration value="fr" />
                       <xs:enumeration value="de" />
                       <xs:enumeration value="sp" />
                   </xs:restriction>
               </xs:simpleType>
           </xs:attribute>
           <xs:attribute name="From" type="xs:string" />
           <xs:attribute name="To" type="xs:string" />
           <xs:attribute name="FromTrunk" type="xs:string" />
           <xs:attribute name="ToTrunk" type="xs:string" />
           <xs:attribute name="FromUser" type="xs:string" />
           <xs:attribute name="ToUser" type="xs:string" />
           <xs:attribute name="IVRUser" type="xs:string" />
           <xs:attribute name="ChargeAccount" type="xs:string" />
           <xs:attribute name="ChargeNumber" type="xs:string" />
           <xs:attribute name="TimeStart" type="xs:unsignedLong" />
           <xs:attribute name="TimeConnected" type="xs:unsignedLong" />
           <xs:attribute name="TimeAnswered" type="xs:unsignedLong" />
           <xs:attribute name="TimeEnd" type="xs:unsignedLong" />
           <xs:attribute name="LocalTime" type="xs:date" />
           <xs:attribute name="Duration" type="xs:string" />
           <xs:attribute name="Extension" type="xs:string" />
           <xs:attribute name="Number" type="xs:string" />
           <xs:attribute name="StatisticsForward" type="xs:string" />
           <xs:attribute name="StatisticsReverse" type="xs:string" />
       </xs:complexType>
   </xs:element>
 </schema>

The format follows the SOAP specification. In the pbxnsip namespace, the record CDR indicates that a CDR shall be transmitted. The CDR may have the following attributes:

CallID The Call-ID for the call. This makes it easier to match the call (like a session-cookie).
Type The PBX reports the last type of the call. This way, it is for example possible to tell if the call went to the voicemail box.
Domain The domain in which the call was being run.
Language If a specific language was selected, this tag will indicate the language.
From The value of the From header as it appeared in the call.
To Similar to the From but for the To header.
FromUser If the call was coming from a known extension, the extension name is reported here.
ToUser If the call was going to a known extension, the extension name is reported here.
FromUser If the call was coming from a known trunk, the trunk name is reported here.
ToUser If the call was going to a known trunk, the trunk name is reported here.
ChargeAccount and ChargeNumber If the call was redirected to an external number, the charged account and the destination number are reported here.
TimeStart The time when the call started. All timestamps are in number of seconds since 1970 (UNIX timestamp).
TimeConnected If the call was connected, this stamp indicates the connection time.
TimeEnd The time when the call was disconnected or cancelled.