Yondertele Call Data API
https://api.yondertele.com gives you programmatic access to your
organization's call records, call recordings, and voicemails — for
reporting, compliance, CRM/ATS integration, or your own dashboards. Every
credential is scoped to your organization: you only ever see your own data.
Questions or trouble? support@yondertele.com
Quick start
Three requests and you've seen everything the API does:
export KEY="ytk_..." # your API key
# 1. Your 10 most recent calls
curl -H "Authorization: Bearer $KEY" \
"https://api.yondertele.com/v1/cdr?limit=10"
# 2. Pick a call that has a recording_id, download its audio
curl -H "Authorization: Bearer $KEY" -o call.ogg \
"https://api.yondertele.com/v1/recordings/55021/audio"
# 3. Voicemails left in the last day
curl -H "Authorization: Bearer $KEY" \
"https://api.yondertele.com/v1/voicemails?since=$(date -u -d yesterday +%FT%TZ)"
New calls appear in the API within one to two minutes of hangup; recording audio is playable within three to four minutes.
Authentication
Every request carries your API key in the Authorization header:
Authorization: Bearer ytk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- Keys are shown once at creation — store them in a secrets manager, never in client-side code or a repository.
- You can hold up to 5 active keys, so rotation is zero-downtime: issue a new key, switch your integration over, revoke the old one.
- Issue and revoke keys yourself in the dashboard
under API access, or programmatically via
/v1/keys. - Requests without a valid key receive
401.
Endpoints at a glance
| Method | Path | What it does |
|---|---|---|
| GET | /v1/cdr |
List call records (paged, filterable) |
| GET | /v1/cdr/{id} |
One call record |
| GET | /v1/recordings/{id}/audio |
Stream call-recording audio (Ogg/Opus) |
| GET | /v1/voicemails |
List voicemail messages (paged) |
| GET | /v1/voicemails/{id}/audio |
Stream voicemail audio (Ogg/Opus) |
| GET | /v1/keys |
List your API keys (metadata only) |
| POST | /v1/keys |
Issue a new key (token shown once) |
| DELETE | /v1/keys/{key_id} |
Revoke a key |
| POST | /v1/dashboard/login |
Exchange username/password for a session token |
| POST | /v1/dashboard/reset-request |
Start account recovery (emails a reset link) |
| POST | /v1/dashboard/reset-confirm |
Set a new password with an emailed token |
| GET | /healthz |
Service health (no auth) |
All timestamps everywhere are ISO 8601 UTC (e.g. 2026-08-12T14:00:00Z) —
convert to local time in your application.
Call records
GET /v1/cdr
Query parameters (all optional, freely combinable):
| Param | Example | Meaning |
|---|---|---|
from |
2026-08-01T00:00:00Z |
Only calls starting at/after this time |
to |
2026-08-31T23:59:59Z |
Only calls starting before this time |
direction |
inbound |
inbound, outbound, or local (internal) |
src |
1001 |
Caller number/extension — matches anywhere in the number |
dst |
7121 |
Destination number/extension — matches anywhere in the number |
number |
8325551234 |
Matches caller or destination — anywhere in the number |
since |
2026-08-12T00:00:00Z |
Incremental-sync mode — see below |
cursor |
eyJ0cyI6… |
Opaque page cursor from the previous response |
limit |
200 |
Page size — default 100, max 500 |
Example — August's inbound calls, 200 per page:
curl -H "Authorization: Bearer $KEY" \
"https://api.yondertele.com/v1/cdr?from=2026-08-01T00:00:00Z&direction=inbound&limit=200"
Response:
{
"items": [
{
"id": 88123,
"host": "usachic001",
"source_uuid": "c7a9f2b1-4e3d-4f6a-9c2e-1b8d7a6e5f40",
"start_time": "2026-08-12T13:58:12Z",
"direction": "inbound",
"src": "8325551234",
"dst": "7121",
"duration_sec": 84,
"billsec": 71,
"disposition": "answered",
"recording_id": 55021
}
],
"next_cursor": "eyJ0cyI6MTc1NDA1MDY5Mn0"
}
Field reference:
| Field | Type | Meaning |
|---|---|---|
id |
integer | Stable unique id — use it for /v1/cdr/{id} and deduplication |
source_uuid |
string | The PBX's own id for the call (also stable/unique) |
start_time |
ISO 8601 | When the call started (UTC) |
direction |
string | inbound / outbound / local, occasionally null |
src, dst |
string | Calling and called number/extension as the PBX saw them |
duration_sec |
integer | Total call length, including ring time |
billsec |
integer | Talk time after answer (0 = never answered) |
disposition |
string | Outcome, e.g. answered, no answer, busy, failed |
recording_id |
integer | Id for /v1/recordings/{id}/audio; null if not recorded |
GET /v1/cdr/export.csv — download a report
Same filters as GET /v1/cdr (from, to, direction, src, dst,
number), no pagination — one CSV file with everything that matches,
oldest first. Times are UTC. The dashboard's Download CSV button on the
Calls page is this endpoint.
curl -H "Authorization: Bearer $KEY" -o august_calls.csv \
"https://api.yondertele.com/v1/cdr/export.csv?from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z"
Columns: id, start_time_utc, direction, from, to, duration_seconds,
talk_seconds, result, recording_id. Exports are capped at 200,000 rows —
if the response carries X-Export-Truncated: true, narrow the date range
and export in slices.
Paging through history (newest first)
Without since, results are newest-first. Keep following next_cursor
until it is null:
curl -H "Authorization: Bearer $KEY" \
"https://api.yondertele.com/v1/cdr?from=2026-07-01T00:00:00Z&limit=200"
# → take next_cursor from the response, then:
curl -H "Authorization: Bearer $KEY" \
"https://api.yondertele.com/v1/cdr?from=2026-07-01T00:00:00Z&limit=200&cursor=eyJ0cyI6…"
Incremental sync — the pattern for CRM/ATS integrations
With since, results flip to oldest-first and strictly after the given
time — built for a repeating sync job. The loop is:
- Call
/v1/cdr?since=<checkpoint>&limit=500. - Process each row; keep following
next_cursoruntil it'snull. - Store the
start_timeof the last row you processed as the new checkpoint. Next run, pass it assince.
since is strictly-after and rows are deduplicable by id, so the loop can
run every minute or once a day — it never misses a call and never double-counts.
import requests
BASE = "https://api.yondertele.com"
HDRS = {"Authorization": "Bearer ytk_..."}
def sync_calls(checkpoint: str) -> str:
"""Pull everything after `checkpoint`; returns the new checkpoint."""
params = {"since": checkpoint, "limit": 500}
while True:
page = requests.get(f"{BASE}/v1/cdr", headers=HDRS,
params=params, timeout=30).json()
for call in page["items"]:
handle(call) # your code
checkpoint = call["start_time"]
if not page["next_cursor"]:
return checkpoint
params["cursor"] = page["next_cursor"]
The same in Node.js:
const BASE = "https://api.yondertele.com";
const HDRS = { Authorization: "Bearer ytk_..." };
async function syncCalls(checkpoint) {
let params = new URLSearchParams({ since: checkpoint, limit: "500" });
while (true) {
const page = await (await fetch(`${BASE}/v1/cdr?${params}`, { headers: HDRS })).json();
for (const call of page.items) {
await handle(call); // your code
checkpoint = call.start_time;
}
if (!page.next_cursor) return checkpoint;
params.set("cursor", page.next_cursor);
}
}
GET /v1/cdr/{id}
One call record by id — same shape as an items element. 404 if the id
doesn't exist (or isn't yours).
Call recordings
Retention notice. All call recordings and voicemails are made and retained for quality assurance and training purposes only. Every audio response carries this text in an
X-Recording-Noticeheader — if your integration surfaces recordings to end users, surface the notice with them.
GET /v1/recordings/{id}/audio
Streams the recording as Ogg/Opus (Content-Type: audio/ogg). The id
comes from a call record's recording_id. HTTP Range requests are supported,
so browser <audio> players and VLC can seek without downloading the file.
curl -H "Authorization: Bearer $KEY" -o call.ogg \
"https://api.yondertele.com/v1/recordings/55021/audio"
Ogg/Opus plays natively in Chrome, Firefox, Edge, and VLC. Need another format? Convert locally:
ffmpeg -i call.ogg call.mp3 # mp3
ffmpeg -i call.ogg -ar 8000 call.wav # 8 kHz wav for telephony tools
Fetching audio from JavaScript needs the Authorization header, which an
<audio src=…> tag can't carry — fetch a blob instead:
const r = await fetch(`${BASE}/v1/recordings/${id}/audio`, { headers: HDRS });
audioElement.src = URL.createObjectURL(await r.blob());
POST /v1/recordings/archive.zip — bulk download
Downloads many recordings as one ZIP. The body is either an explicit id
list, or the same filter fields as GET /v1/cdr to fetch everything that
matches:
# Everything from a date range
curl -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"from":"2026-08-01T00:00:00Z","to":"2026-08-08T00:00:00Z"}' \
-o recordings.zip https://api.yondertele.com/v1/recordings/archive.zip
# A specific set, by recording_id
curl -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"ids":[55021,55022,55038]}' \
-o recordings.zip https://api.yondertele.com/v1/recordings/archive.zip
Filter fields (when ids is absent): from, to, direction, src,
dst, number — identical semantics to GET /v1/cdr.
The ZIP contains one .ogg per recording (named
call_<UTC time>_<from>_to_<to>_<recording_id>.ogg), a manifest.csv
mapping every file back to its call record, and NOTICE.txt with the
retention notice. Archives are capped at 500 files / 512 MB — a capped
response sets X-Export-Truncated: true; narrow the range and request the
rest. 404 means nothing matched.
Voicemails work the same way at POST /v1/voicemails/archive.zip, with
body fields ids or mailbox / from / to.
Voicemails
GET /v1/voicemails
Same paging model as /v1/cdr (since / cursor / limit), plus
mailbox to filter to one extension:
curl -H "Authorization: Bearer $KEY" \
"https://api.yondertele.com/v1/voicemails?mailbox=1001&limit=50"
Response:
{
"items": [
{
"id": 9021,
"host": "usachic001",
"mailbox": "1001",
"caller_id": "\"WIRELESS CALLER\" <8325551234>",
"left_at": "2026-08-12T15:22:04Z",
"duration_sec": 34,
"bytes": 68227
}
],
"next_cursor": null
}
Audio at /v1/voicemails/{id}/audio — Ogg/Opus with Range support, exactly
like call recordings.
Managing API keys
Self-service, using any of your existing keys (or a dashboard session):
# List your keys — metadata only, tokens are never shown again
curl -H "Authorization: Bearer $KEY" https://api.yondertele.com/v1/keys
# Issue a new key (the token in the response is shown exactly once)
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"label":"crm-sync"}' https://api.yondertele.com/v1/keys
# Revoke a key by its key_id (from the list call)
curl -X DELETE -H "Authorization: Bearer $KEY" \
https://api.yondertele.com/v1/keys/2f6c0d7e-...
Up to 5 keys can be active at once. Rotation without downtime: issue → switch your integration → revoke the old key. The same controls live in the dashboard's API access tab.
Dashboard and account recovery
Everything the API serves is also browsable by humans at https://api.yondertele.com/dashboard/ — sign in with your email address and the password Yondertele issued, browse calls, play recordings, and manage API keys.
Changing your password: once signed in, open the Account page and set a new password (your current one is required; minimum 10 characters). Every other signed-in device is logged out; the session you changed it from stays active.
Forgot your password? Click Forgot password? on the sign-in screen and enter your email address. We send — from support@yondertele.com — a reset link that expires in 30 minutes and works once. The form runs an invisible browser check (about a second, nothing to click) to keep bots from abusing it. Setting a new password signs out all existing sessions. Accounts created before August 2026 may have a plain username instead of an email; it still works everywhere an email is asked for, and support can switch you over.
Sessions sign out automatically after 5 minutes of inactivity (and cap at 30 days regardless). API keys are unaffected — integrations don't idle out.
Rate limits, errors, and retries
| Limit | Value |
|---|---|
| API requests | 120 / minute per key |
| Audio downloads | 10 / minute per key |
| Status | Meaning | What to do |
|---|---|---|
401 |
Missing, invalid, or revoked key | Check the header; issue a new key if revoked |
404 |
Record doesn't exist or isn't yours | The API never confirms other tenants' data exists — treat as "not found" |
422 |
Malformed parameter (e.g. bad timestamp) | Fix the request; the detail field says which parameter |
429 |
Rate limit hit | Back off and retry after 60 seconds |
5xx |
Transient server error | Retry with exponential backoff |
Error responses are JSON: {"detail": "human-readable explanation"}.
Data freshness and retention
- Call records: visible in the API typically 1–2 minutes after hangup.
- Recordings: playable typically 3–4 minutes after hangup.
- Recordings and voicemails are archived centrally and retained per your service agreement; call records are retained indefinitely.
- The archive is fed continuously from the phone system — you never need to ask for a "refresh."
Good-citizen checklist
- Use
since+next_cursorfor sync jobs; don't re-scan history each run. - Deduplicate on
id(orsource_uuid) if your pipeline can replay. - Store keys server-side only; rotate via the two-key overlap pattern.
- Respect
429with a 60-second backoff.