Description
Exports call history as a downloadable file. Uses the same
GET /v1/voice/calls endpoint as List Call History,
but switches to export mode when the format query parameter is set to
csv, xlsx, or json.
Exports paginate through the full result set server-side (no 1000 row cap)
and apply every filter supported by the list endpoint.
- CSV — streamed row-by-row, safe for very large exports.
- XLSX — buffered Excel workbook (
.xlsx), good for most business reports.
- JSON — single JSON document with a
calls array; use for programmatic
consumption.
Safety Cap
Exports are capped at 50,000 rows per call. When the cap is hit, the
response still returns 200 with the first 50,000 matching rows and the
X-Export-Truncated response header set to true. Narrow the time window
or add filters to page through larger result sets.
Authentication
Query Parameters
Export Control
Export format. One of: csv, xlsx, json. Omit to use the standard
paginated JSON list response instead.
Comma-separated list of column ids to include, in order. Omit to use the
default set.Valid ids:
call_id, start_time, end_time, duration_seconds, from_number,
to_number, direction, agent_name, user_sentiment, call_successful,
in_voicemail, call_status, recording_url, campaign_id, contact_id,
transcript.Default:
call_id, start_time, duration_seconds, from_number, to_number, direction, agent_name, user_sentiment, call_successful, in_voicemail, recording_url, campaign_id
When true, each distinct key observed across all exported calls’
extracted_fields becomes its own column, named extracted.{key} (CSV/XLSX)
or grouped under an extracted_fields object (JSON). Set to false to
omit extracted data entirely.
Scope
Organization unique_id to filter calls
User unique_id (required for non-admin users)
true returns all organization calls; false restricts to the caller’s own calls
Filters (identical to the list endpoint)
Filter by agent (unique_id or internal UUID)
Positive, Negative, Neutral, or Unknown
true to include only calls with a recording URL; false for calls without
Substring match against from_number OR to_number. The leading + is
stripped, so 4155551234 and +4155551234 behave the same.
Minimum call_duration_ms (inclusive)
Maximum call_duration_ms (inclusive)
ISO-8601 lower bound on call start time (e.g., 2026-04-01T00:00:00Z)
ISO-8601 upper bound on call start time
Every export response includes these headers:
| Header | Description |
|---|
Content-Type | text/csv; charset=utf-8, the XLSX MIME type, or application/json; charset=utf-8 |
Content-Disposition | attachment; filename="call-history-YYYYMMDD-HHMMSS.<ext>" |
X-Export-Row-Count | Number of rows included in the body |
X-Export-Row-Cap | The server-side safety cap (currently 50000) |
X-Export-Truncated | true when the cap was reached, otherwise false |
Example Requests
CSV export (defaults)
curl -X GET "https://api.teli.ai/v1/voice/calls?format=csv&organization_id=ORG_ID&is_admin=true&start_date=2026-04-01T00:00:00Z&end_date=2026-04-22T23:59:59Z" \
-H "X-API-Key: YOUR_API_KEY" \
-o call-history.csv
XLSX export with custom columns
curl -X GET "https://api.teli.ai/v1/voice/calls?format=xlsx&organization_id=ORG_ID&is_admin=true&columns=call_id,start_time,from_number,to_number,duration_seconds,user_sentiment,transcript&include_extracted_fields=true" \
-H "X-API-Key: YOUR_API_KEY" \
-o call-history.xlsx
JSON export with filters
curl -X GET "https://api.teli.ai/v1/voice/calls?format=json&organization_id=ORG_ID&is_admin=true&direction=outbound&sentiment=Positive&has_recording=true" \
-H "X-API-Key: YOUR_API_KEY" \
-o call-history.json
const params = new URLSearchParams({
format: 'csv',
organization_id: orgId,
is_admin: 'true',
start_date: '2026-04-01T00:00:00Z',
end_date: '2026-04-22T23:59:59Z',
columns: 'call_id,start_time,from_number,to_number,duration_seconds,user_sentiment'
});
const response = await fetch(`https://api.teli.ai/v1/voice/calls?${params}`, {
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
const truncated = response.headers.get('X-Export-Truncated') === 'true';
const rowCount = response.headers.get('X-Export-Row-Count');
const blob = await response.blob();
// ...save or display the file
import requests
params = {
"format": "xlsx",
"organization_id": org_id,
"is_admin": "true",
"start_date": "2026-04-01T00:00:00Z",
"end_date": "2026-04-22T23:59:59Z",
"include_extracted_fields": "true",
}
response = requests.get(
"https://api.teli.ai/v1/voice/calls",
params=params,
headers={"X-API-Key": "YOUR_API_KEY"},
stream=True,
)
with open("call-history.xlsx", "wb") as f:
for chunk in response.iter_content(chunk_size=65536):
f.write(chunk)
print("Rows:", response.headers.get("X-Export-Row-Count"))
print("Truncated:", response.headers.get("X-Export-Truncated"))
Example Responses
CSV body (abbreviated)
Call ID,Start Time (UTC),Duration (s),From,To,Direction,Agent,Sentiment,Successful,Voicemail,Recording URL,Campaign ID,extracted.first_name,extracted.budget
call_d35fab7c6bd8cf9aee6c322b7a4,2026-04-15T19:45:03Z,125,+15174686941,+15551234567,outbound,Sales Agent,Positive,true,false,https://...,camp_123,Jane,250000
call_9a2b0...,2026-04-15T19:52:11Z,42,+15174686941,+15555550104,outbound,Sales Agent,Neutral,false,true,,camp_123,,
JSON body
{
"count": 2,
"truncated": false,
"calls": [
{
"call_id": "call_d35fab7c6bd8cf9aee6c322b7a4",
"start_time": "2026-04-15T19:45:03Z",
"duration_seconds": 125,
"from_number": "+15174686941",
"to_number": "+15551234567",
"direction": "outbound",
"agent_name": "Sales Agent",
"user_sentiment": "Positive",
"call_successful": true,
"in_voicemail": false,
"recording_url": "https://teli-voice-recordings-prod.s3.us-west-2.amazonaws.com/recordings/call_d35fab7c6bd8cf9aee6c322b7a4.wav",
"campaign_id": "camp_123",
"extracted_fields": {
"first_name": "Jane",
"budget": "250000"
}
},
{
"call_id": "call_9a2b0...",
"start_time": "2026-04-15T19:52:11Z",
"duration_seconds": 42,
"from_number": "+15174686941",
"to_number": "+15555550104",
"direction": "outbound",
"agent_name": "Sales Agent",
"user_sentiment": "Neutral",
"call_successful": false,
"in_voicemail": true,
"recording_url": null,
"campaign_id": "camp_123",
"extracted_fields": {}
}
]
}
Missing organization_id
{
"error": "organization_id is required",
"success": false,
"powered_by": "Teli"
}
Organization not found
{
"error": "Organization not found",
"success": false,
"powered_by": "Teli"
}
Behavior
- Filter parity — every filter accepted by
GET /v1/voice/calls also
applies to the export. Exports and the paginated list view use the same
query contract.
- No row cap surprise — exports paginate through Teli’s database
server-side in 500-row pages. The only limit is the 50,000-row safety cap
per request.
- Tenant- and org-scoped — only calls belonging to the calling tenant
and the
organization_id you specify are included. Non-admin callers are
further restricted to their own user_id.
- Phantom-call filtering — zero-duration fork artifacts are excluded
from exports, matching the list endpoint.
- Dynamic extracted columns — when
include_extracted_fields=true,
the exporter scans every exported row and emits one column per distinct
extracted_fields key observed across the full result set.
- Transcripts — opt in by adding
transcript to the columns list.
Transcripts can be long and will increase file size substantially.
If X-Export-Truncated is true, you have more than 50,000 matching calls
for the given filters. Paginate by narrowing the date range:
Pass 1: start_date=2026-04-01&end_date=2026-04-07T23:59:59Z
Pass 2: start_date=2026-04-08&end_date=2026-04-14T23:59:59Z
Pass 3: start_date=2026-04-15&end_date=2026-04-21T23:59:59Z
GET /v1/voice/calls — paginated JSON list of calls (same filters)
GET /v1/voice/calls/{call_id} — full call detail, including transcript
GET /v1/voice/calls/{call_id}/recording — signed recording URL
GET /v1/voice/calls/{call_id}/extractions — structured extraction for one call