Recordings
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 (optional ?agentId=) |
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 |
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.