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.
24 mo
API query window may vary by plan
/min
Connected talk time
6s
After initial period
GET
/v1/sip/cdr
Authentication
Authorization: Basic <token>. Auth guide →List call detail records
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.
| Field | Type | Required | Description |
|---|---|---|---|
| from | string (ISO 8601) | required | Start of range (inclusive) on call start time. |
| to | string (ISO 8601) | required | End of range (exclusive) on call start time. |
| trunkId | string | optional | Limit to a single trunk. |
| direction | string | optional | inbound | outbound |
| status | string | optional | answered | no_answer | busy | failed | cancelled | in_progress |
| number | string | optional | Match from or to party (E.164). |
| callId | string | optional | Fetch a single Sendexa callId. |
| limit | integer | optional | Page size 1–200 (default 50). |
| cursor | string | optional | Opaque pagination cursor from the previous response. |
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
{"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
| Field | Type | Required | Description |
|---|---|---|---|
| callId | string | required | Sendexa unique call identifier. Use for support tickets and reconciliation. |
| sipCallId | string | required | SIP Call-ID header value. Correlate with PBX logs and PCAP. |
| trunkId | string | required | Trunk that authenticated or owned the DID. |
| direction | string | required | inbound (origination) or outbound (termination). |
| status | string | required | Final or current call status (see status table below). |
| from.number / to.number | string | required | Parties in E.164 when known. |
| did | string | optional | For inbound: the Sendexa DID that was dialled. Null on pure outbound. |
| timestamps.* | string | required | startedAt, ringingAt, answeredAt, endedAt — UTC ISO 8601. |
| durationSeconds | integer | required | Talk time from answer to hangup (0 if never answered). |
| billableSeconds | integer | required | Duration after minimum + increment rounding used for cost. |
| hangupCause | string | required | Symbolic cause (NORMAL_CLEARING, USER_BUSY, NO_ANSWER, …). |
| hangupCauseCode | integer | optional | Q.850-style cause code when available (e.g. 16, 17, 19). |
| hangupOrigin | string | optional | local | remote | network — who initiated BYE/CANCEL. |
| codec | string | optional | Negotiated audio codec (PCMU, PCMA, G729, opus). |
| transport | string | optional | udp | tcp | tls used on the Sendexa-facing leg. |
| failoverUsed | boolean | optional | True if inbound hunt moved to failoverUri. |
| destinationUri | string | optional | Final SIP URI contacted for inbound origination. |
| attestation | string | optional | STIR/SHAKEN level A | B | C when signing applied. |
| billing | object | required | ratePerMinute, billedMinutes, totalCost, currency, incrementSeconds, minimumSeconds. |
| quality | object | optional | Optional mosEstimate, packetLossPercent, jitterMs when media stats available. |
Call statuses
| Status | Meaning | Typically billed? |
|---|---|---|
| in_progress | Dialog still active (rare in historical queries) | Partial / finalised at end |
| answered | Call connected; talk time > 0 | Yes — billableSeconds |
| no_answer | Rang but not answered before timeout | No talk-time charge |
| busy | 486 Busy or equivalent | No |
| cancelled | Originator CANCEL before answer | No |
| failed | Routing, auth, capacity, or network failure | No (inspect hangupCause) |
Common hangup causes
| hangupCause | Code | Typical meaning |
|---|---|---|
NORMAL_CLEARING | 16 | Clean hangup after connect |
USER_BUSY | 17 | Callee busy |
NO_USER_RESPONSE | 18 | No response from user equipment |
NO_ANSWER | 19 | Alerted but not answered |
CALL_REJECTED | 21 | Call rejected by far end |
INVALID_NUMBER_FORMAT | 28 | Number not E.164 / unroutable |
NORMAL_TEMPORARY_FAILURE | 41 | Temporary network issue |
NO_CIRCUIT_AVAILABLE | 34 | Capacity / circuit congestion |
BEARER_CAPABILITY_NOT_AUTH | 57 | Auth / permission (CLI, trunk) |
ORIGINATOR_CANCEL | — | Caller cancelled before answer |
RECOVERY_ON_TIMER_EXPIRE | 102 | SIP 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.
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
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;}
- 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.
Insufficient balance
failed with an auth/billing cause.Fetch a single call
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
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 monthconst 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);
callId, billableSeconds, totalCost, …).Reconciliation tips
API errors
| HTTP | Code | Cause |
|---|---|---|
| 400 | INVALID_DATE_RANGE | from/to missing, inverted, or > 31 days |
| 400 | INVALID_FILTER | Unknown direction/status value |
| 401 | UNAUTHORIZED | Missing or invalid Basic token |
| 403 | FORBIDDEN | Token lacks SIP CDR scope |
| 404 | CALL_NOT_FOUND | callId does not exist on this account |
| 429 | RATE_LIMIT_EXCEEDED | Slow down list polling; use webhooks/export |
Best practices
- Store
callIdandsipCallIdin your own CDR for dual-sided reconciliation - Pull CDR daily with a stable UTC window; avoid overlapping ranges that double-count
- Alert on elevated
failedrates or suddenUSER_BUSYspikes (capacity) - Use
quality.mosEstimatetrends to catch network issues early - Treat
billing.totalCostas authoritative for usage charges