Recordings API
List call recordings and fetch time‑limited download URLs. All endpoints are organization‑scoped and return the dashboard envelope { "success": true, "data": { … } }.
Auth: an API key (X-API-Key, org implicit) or a user JWT plus X-Org-Id. Reads work with any valid key; POST /upload requires an owner, admin, or developer key. A key is pinned to its own org — another org's {orgId} in the path returns 403.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/orgs/{orgId}/recordings | List recordings, newest first (optional ?agentId=, ?sessionId=, ?limit=, ?cursor=) |
GET | /api/orgs/{orgId}/recordings/{id} | Recording metadata |
GET | /api/orgs/{orgId}/recordings/{id}/signed-url | Get a short‑lived download URL |
GET | /api/orgs/{orgId}/recordings/{id}/file | Stream the audio (or redirect to storage) |
POST | /api/orgs/{orgId}/recordings/upload | Upload a recording (multipart) |
GET | /api/orgs/{orgId}/call-audio/{id}/file | Ad‑blocker‑safe alias of …/recordings/{id}/file |
GET | /api/orgs/{orgId}/call-audio/{id}/signed-url | Ad‑blocker‑safe alias of …/signed-url |
Paging and one call's recordings
Without parameters the list returns the newest 200 recordings. To read further back, or to read one call's parts, pass any of:
sessionId— only that call's recordings;limit— page size, at most 200;cursor— thedata.nextCursorof the page before; the next page starts just after its last recording.
nextCursor is set while a page came back full and null on the last page. Recordings stored
after you started paging appear at the top of the list, never inside the pages behind your cursor.
Use /call-audio in browser code
This is the single most common "recording won't load" cause, and it is not a Telenow bug.
EasyPrivacy‑class filter lists (uBlock Origin, AdGuard, Brave Shields) block any URL containing recording — they're targeting session‑replay trackers like Hotjar and FullStory, and our path is collateral damage. The request never reaches the server; you see a failed XHR in devtools and an empty player, while the same URL fetched with curl returns 200.
/call-audio/{id}/file and /call-audio/{id}/signed-url are the same handlers behind the same auth — only the path differs, chosen to survive those filter lists. Rules of thumb:
- Browser/XHR/
<audio src>→ use/call-audio/…. - Server‑to‑server → either works;
/recordings/…is fine and stays supported.
If a customer reports "Recording could not be loaded", have them try in a private window with extensions disabled before you investigate storage.
Downloading audio
Recordings are not public. Request a signed URL (valid 5 minutes) per recording id and use it immediately:
curl https://api.telenow.ai/api/orgs/{orgId}/recordings/{id}/signed-url \
-H "X-API-Key: vai_live_…"
{ "success": true, "data": { "url": "https://…", "expiresAt": "2026-06-08T12:34:56Z" } }
GET …/{id}/file streams the bytes directly (local storage) or redirects to a signed storage URL (S3‑style backends) — useful for an <audio src>. It's JWT/key‑protected, so it can't be embedded in a public page; for that, fetch a signed-url instead.
Metadata
GET /api/orgs/{orgId}/recordings/{id}
The recording row carries: id, org_id, the session_id it belongs to, agent_id, channel (e.g. softphone, web, telephony), storage_kind, mime, sample_rate, duration_sec, size_bytes, metadata, and created_at. List with the org endpoint and filter by agent via ?agentId=:
{ "success": true, "data": { "recordings": [ /* … */ ], "total": 12 } }
Recordings on the call detail (the two‑recording case)
A single call can have more than one recording. The most common reason is a warm transfer: the AI portion (Telenow's in‑house mix, uploaded when the stream stops at transfer/hangup) and the bridged human portion (the carrier's own recording of the caller talking to the human, ingested after the transfer) are stored as two rows.
The single recording_id on the call (the latest) stays for back‑compat, but the call detail endpoint returns all of a call's recordings as an array, oldest first, so neither half is hidden:
GET /api/orgs/{orgId}/calls/{id}
{
"success": true,
"data": {
"id": "…",
"recording_id": "…",
"recordings": [
{
"id": "…",
"channel": "telephony",
"duration_sec": 92,
"created_at": "2026-06-08T12:30:11Z",
"source": null
},
{
"id": "…",
"channel": "telephony",
"duration_sec": 145,
"created_at": "2026-06-08T12:31:48Z",
"source": "transfer_bridge"
}
]
}
}
The source field marks the post‑transfer carrier recording as "transfer_bridge"; the AI conversation has source: null. Each part is downloaded by its own recording id through /recordings/{id}/signed-url (or /file) — there is no per‑call audio endpoint. Calls without a transfer return a single‑element array (or an empty one when nothing was recorded).
Uploading
POST /api/orgs/{orgId}/recordings/upload accepts multipart/form-data. This is how the browser web‑call widget stores its locally‑mixed HD audio; most server‑side recordings (softphone, telephony, transfer bridges) are created automatically and you never call this.
| Part | Required | Default | Notes |
|---|---|---|---|
file | yes | — | The audio bytes. |
session_id | no | — | Links the recording to a call. Without it the row is orphaned and won't appear on any call detail. |
agent_id | no | — | Enables ?agentId= filtering on the list endpoint. |
channel | no | web | web, softphone, telephony. |
mime | no | audio/wav | Set it if you upload anything else, or players may refuse the file. |
sample_rate | no | — | Hz, e.g. 24000. |
duration_sec | no | — | Seconds. See the warning below. |
part | no | — | Distinguishes multiple uploads for one session. |
Send a real
duration_sec. If it's absent or wrong, the recording can be stored with a duration derived from an epoch timestamp — which surfaces in the dashboard as an absurd length and breaks the player's seek bar. Pass the actual measured duration in seconds.
Limits and errors
| Status | Meaning |
|---|---|
201 | Created — returns the new recording row. |
400 | Malformed multipart, or no file part. |
403 | Key role is read‑only (viewer/member), or the key belongs to another org. |
413 | Body over the 100 MB limit. Split long calls, or upload a compressed format. |
Errors (all endpoints)
| Status | Meaning | Fix |
|---|---|---|
401 | Access token is required | You sent no credential, or a JWT without X-Org-Id. |
401 | Invalid or revoked API key | Check the key is complete — secrets are 52 characters and contain -/_, so a truncated paste looks like a revoked key. |
403 | This API key is scoped to a different organization. | The {orgId} in the path isn't the key's org. |
404 | recording not found | Wrong id, or it belongs to another org — the lookup is org‑scoped, so cross‑org ids read as "not found" rather than confirming existence. |
500 | Something went wrong | Generic. For recordings this is usually a storage backend failure (S3 unreachable, object missing for the row's storage_kind). The specific cause is server‑side only — it's logged, never returned. If it's reproducible for one recording id, that row's bytes are likely gone. |
recording.ready webhook
When a recording is finalized — including ones that land shortly after a call ends, such as a transfer bridge — Telenow fires the recording.ready webhook carrying a signed URL, so you can ingest audio without polling.
Related
- Calls, recordings & insights — where recordings surface in the dashboard.
- Making & receiving calls — how transfers produce two recordings.
- Sessions & calls API — the call detail object the
recordings[]array lives on.