SIP Trunking
Preview
SIP

CDR & Reporting

Call detail records for Sendexa SIP trunks. Retrieve per-call analytics, reconcile per-minute billing, and troubleshoot hangups with structured fields and filters.

Complete CDRs

Every attempt and completed call with timestamps, parties, codec, and cause codes.

Billing fields

Billed duration, rate, currency, and total cost for finance reconciliation.

Rich filters

Filter by trunk, direction, number, status, and time range.

Export ready

Page through results or export CSV from the dashboard for BI tools.

Retention

24 mo

API query window may vary by plan

Billing unit

/min

Connected talk time

Increment

6s

After initial period

Endpoint

GET

/v1/sip/cdr

List call detail records

GET
/v1/sip/cdr

Returns a paginated list of CDR objects for your business. All timestamps are UTC ISO 8601. Maximum range per request is 31 days; page with cursor for large exports.

FieldTypeRequiredDescription
fromstring (ISO 8601)
required
Start of range (inclusive) on call start time.
tostring (ISO 8601)
required
End of range (exclusive) on call start time.
trunkIdstringoptionalLimit to a single trunk.
directionstringoptionalinbound | outbound
statusstringoptionalanswered | no_answer | busy | failed | cancelled | in_progress
numberstringoptionalMatch from or to party (E.164).
callIdstringoptionalFetch a single Sendexa callId.
limitintegeroptionalPage size 1–200 (default 50).
cursorstringoptionalOpaque pagination cursor from the previous response.
Bash
curl -G 'https://api.sendexa.co/v1/sip/cdr' \
-H 'Authorization: Basic YOUR_DASHBOARD_BASE64_TOKEN' \
--data-urlencode 'from=2026-07-01T00:00:00Z' \
--data-urlencode 'to=2026-07-12T00:00:00Z' \
--data-urlencode 'trunkId=TRK-8c1e2b9a-4d3f-41a0-9e7c-12ab34cd56ef' \
--data-urlencode 'direction=outbound' \
--data-urlencode 'status=answered' \
--data-urlencode 'limit=50'

Response shape

GET /v1/sip/cdr
{
"success": true,
"code": "OK",
"message": "CDR query successful.",
"requestId": "REQ-cdr-77ab12",
"timestamp": "2026-07-12T12:00:00.000Z",
"data": {
"records": [
{
"callId": "CALL-c0ffee00-1111-2222-3333-444455556666",
"sipCallId": "5f2a9c10@sip.sendexa.co",
"trunkId": "TRK-8c1e2b9a-4d3f-41a0-9e7c-12ab34cd56ef",
"trunkName": "Contact Centre Trunk",
"direction": "outbound",
"status": "answered",
"from": {
"number": "+233302123456",
"name": "My Company",
"presentation": "allowed"
},
"to": {
"number": "+233555539152",
"country": { "code": "GH", "name": "Ghana" },
"network": "MTN"
},
"did": null,
"timestamps": {
"startedAt": "2026-07-11T09:14:02.120Z",
"ringingAt": "2026-07-11T09:14:03.001Z",
"answeredAt": "2026-07-11T09:14:08.440Z",
"endedAt": "2026-07-11T09:16:40.200Z"
},
"durationSeconds": 152,
"billableSeconds": 156,
"ringSeconds": 5,
"hangupCause": "NORMAL_CLEARING",
"hangupCauseCode": 16,
"hangupOrigin": "remote",
"codec": "PCMA",
"transport": "tls",
"failoverUsed": false,
"destinationUri": null,
"attestation": null,
"billing": {
"ratePerMinute": 0.045,
"billedMinutes": 2.6,
"totalCost": 0.117,
"currency": "USD",
"incrementSeconds": 6,
"minimumSeconds": 30
},
"quality": {
"mosEstimate": 4.2,
"packetLossPercent": 0.1,
"jitterMs": 12
},
"metadata": {
"pddMs": 880,
"edgeRegion": "acc"
}
},
{
"callId": "CALL-aa11bb22-3333-4444-5555-666677778888",
"sipCallId": "8ab1@sip.sendexa.co",
"trunkId": "TRK-8c1e2b9a-4d3f-41a0-9e7c-12ab34cd56ef",
"direction": "inbound",
"status": "answered",
"from": {
"number": "+233244000111",
"presentation": "allowed"
},
"to": {
"number": "+233302123456",
"country": { "code": "GH", "name": "Ghana" }
},
"did": "+233302123456",
"timestamps": {
"startedAt": "2026-07-11T10:01:00.000Z",
"answeredAt": "2026-07-11T10:01:04.200Z",
"endedAt": "2026-07-11T10:05:04.200Z"
},
"durationSeconds": 240,
"billableSeconds": 240,
"hangupCause": "NORMAL_CLEARING",
"hangupCauseCode": 16,
"hangupOrigin": "local",
"codec": "PCMU",
"transport": "tls",
"failoverUsed": true,
"destinationUri": "sip:233302123456@backup.example.com:5061;transport=tls",
"billing": {
"ratePerMinute": 0.02,
"billedMinutes": 4.0,
"totalCost": 0.08,
"currency": "USD",
"incrementSeconds": 6,
"minimumSeconds": 0
}
}
],
"page": {
"limit": 50,
"nextCursor": "eyJvZmZzZXQiOjUwfQ",
"totalEstimate": 1284
}
}
}

CDR field reference

FieldTypeRequiredDescription
callIdstring
required
Sendexa unique call identifier. Use for support tickets and reconciliation.
sipCallIdstring
required
SIP Call-ID header value. Correlate with PBX logs and PCAP.
trunkIdstring
required
Trunk that authenticated or owned the DID.
directionstring
required
inbound (origination) or outbound (termination).
statusstring
required
Final or current call status (see status table below).
from.number / to.numberstring
required
Parties in E.164 when known.
didstringoptionalFor inbound: the Sendexa DID that was dialled. Null on pure outbound.
timestamps.*string
required
startedAt, ringingAt, answeredAt, endedAt — UTC ISO 8601.
durationSecondsinteger
required
Talk time from answer to hangup (0 if never answered).
billableSecondsinteger
required
Duration after minimum + increment rounding used for cost.
hangupCausestring
required
Symbolic cause (NORMAL_CLEARING, USER_BUSY, NO_ANSWER, …).
hangupCauseCodeintegeroptionalQ.850-style cause code when available (e.g. 16, 17, 19).
hangupOriginstringoptionallocal | remote | network — who initiated BYE/CANCEL.
codecstringoptionalNegotiated audio codec (PCMU, PCMA, G729, opus).
transportstringoptionaludp | tcp | tls used on the Sendexa-facing leg.
failoverUsedbooleanoptionalTrue if inbound hunt moved to failoverUri.
destinationUristringoptionalFinal SIP URI contacted for inbound origination.
attestationstringoptionalSTIR/SHAKEN level A | B | C when signing applied.
billingobject
required
ratePerMinute, billedMinutes, totalCost, currency, incrementSeconds, minimumSeconds.
qualityobjectoptionalOptional mosEstimate, packetLossPercent, jitterMs when media stats available.

Call statuses

StatusMeaningTypically billed?
in_progressDialog still active (rare in historical queries)Partial / finalised at end
answeredCall connected; talk time > 0Yes — billableSeconds
no_answerRang but not answered before timeoutNo talk-time charge
busy486 Busy or equivalentNo
cancelledOriginator CANCEL before answerNo
failedRouting, auth, capacity, or network failureNo (inspect hangupCause)

Common hangup causes

hangupCauseCodeTypical meaning
NORMAL_CLEARING16Clean hangup after connect
USER_BUSY17Callee busy
NO_USER_RESPONSE18No response from user equipment
NO_ANSWER19Alerted but not answered
CALL_REJECTED21Call rejected by far end
INVALID_NUMBER_FORMAT28Number not E.164 / unroutable
NORMAL_TEMPORARY_FAILURE41Temporary network issue
NO_CIRCUIT_AVAILABLE34Capacity / circuit congestion
BEARER_CAPABILITY_NOT_AUTH57Auth / permission (CLI, trunk)
ORIGINATOR_CANCELCaller cancelled before answer
RECOVERY_ON_TIMER_EXPIRE102SIP timer expiry / media timeout

Per-minute billing model

Connected calls are charged based on billableSeconds, derived from answered talk time with destination-specific minimums and increments. Rate cards live in the dashboard under SIP → Rates.

Rounding example

Assume minimum 30s, increment 6s, rate $0.045 / min:

  • Talk 12s → billable 30s → 0.5 min → $0.0225
  • Talk 152s → ceil to 156s (26×6) → 2.6 min → $0.117
  • Talk 0s (no answer) → billable 0 → $0.00
JavaScript
function billableSeconds(talk, min = 30, inc = 6) {
if (talk <= 0) return 0;
if (talk <= min) return min;
return Math.ceil(talk / inc) * inc;
}
function cost(talk, ratePerMin = 0.045) {
const b = billableSeconds(talk);
return (b / 60) * ratePerMin;
}
What is not billed
  • Unanswered, busy, cancelled, or failed attempts (talk time)
  • Auth challenges and OPTIONS keepalives
  • Internal ring time before answer (not talk time)

Number rental / monthly DID fees are separate line items from per-minute usage and appear on invoices, not necessarily on each CDR row.

Fetch a single call

GET
/v1/sip/cdr?callId=
Bash
curl -G 'https://api.sendexa.co/v1/sip/cdr' \
-H 'Authorization: Basic YOUR_DASHBOARD_BASE64_TOKEN' \
--data-urlencode 'callId=CALL-c0ffee00-1111-2222-3333-444455556666'

Pagination & export

JavaScript
async function fetchAllCdr({ token, from, to, trunkId }) {
const records = [];
let cursor;
do {
const params = new URLSearchParams({ from, to, limit: '200' });
if (trunkId) params.set('trunkId', trunkId);
if (cursor) params.set('cursor', cursor);
const res = await fetch(
`https://api.sendexa.co/v1/sip/cdr?${params}`,
{ headers: { Authorization: `Basic ${token}` } }
);
const json = await res.json();
records.push(...json.data.records);
cursor = json.data.page?.nextCursor || null;
} while (cursor);
return records;
}
// Example: sum outbound cost for a month
const rows = await fetchAllCdr({
token: process.env.SENDEXA_TOKEN,
from: '2026-07-01T00:00:00Z',
to: '2026-08-01T00:00:00Z',
trunkId: 'TRK-8c1e2b9a-4d3f-41a0-9e7c-12ab34cd56ef',
});
const outboundCost = rows
.filter((r) => r.direction === 'outbound' && r.status === 'answered')
.reduce((sum, r) => sum + (r.billing?.totalCost || 0), 0);
console.log('Outbound spend', outboundCost);
Dashboard CSV export
For finance teams without code, use SIP → CDR → Export in the Sendexa dashboard. CSV columns map 1:1 to the JSON field names above (callId, billableSeconds, totalCost, …).

Reconciliation tips

Join on sipCallId
Your PBX CDR Call-ID should match sipCallId for the Sendexa-facing leg (allow for B2BUA rewrites — also store callId from any custom header if injected).
Prefer billableSeconds
Do not re-round durationSeconds yourself for cost; use billableSeconds and totalCost from Sendexa as source of truth.
Timezone
All API times are UTC. Convert only at the presentation layer for local reports.
Failover rows
One inbound call produces one CDR even if failoverUri was used; failoverUsed=true and destinationUri show the winning leg.

API errors

HTTPCodeCause
400INVALID_DATE_RANGEfrom/to missing, inverted, or > 31 days
400INVALID_FILTERUnknown direction/status value
401UNAUTHORIZEDMissing or invalid Basic token
403FORBIDDENToken lacks SIP CDR scope
404CALL_NOT_FOUNDcallId does not exist on this account
429RATE_LIMIT_EXCEEDEDSlow down list polling; use webhooks/export