Recordings API
List call recordings and fetch time‑limited download URLs, and choose where recordings go: kept in Telenow, or sent to you with no copy kept. 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, and PUT /delivery an owner or admin one. 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}/recordings/delivery | Where the org's recordings go |
PUT | /api/orgs/{orgId}/recordings/delivery | Choose where they go: kept in Telenow, or sent to you |
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. |
409 | This workspace sends its recordings to its own systems and Telenow keeps no copy, so this recording was not stored. Recordings are sent to you, so nothing is stored. |
Where recordings go
By default Telenow keeps each call recording: you play it in the dashboard, list and download it with the endpoints above, and receive recording.ready with a signed link. An owner or admin can instead have recordings sent to you, with no copy kept in Telenow. Each finished recording then goes to your webhook as a recording.audio event, and Telenow stores nothing: no file and no recording row. In the dashboard this is Settings → Organization → Call recordings → Where recordings go.
This setting only decides where the file goes. Whether a call is recorded at all is still its agent's telephony.recording setting, and on a softphone call the member's Record their softphone calls setting.
| Method | Path | Who |
|---|---|---|
GET | /api/orgs/{orgId}/recordings/delivery | Any member of the org, or any of its API keys |
PUT | /api/orgs/{orgId}/recordings/delivery | Owner or admin. An API key needs the owner or admin role itself |
Read the setting
curl https://api.telenow.ai/api/orgs/{orgId}/recordings/delivery \
-H "X-API-Key: $TELENOW_API_KEY"
{
"success": true,
"data": {
"destination": "telenow",
"sendModeAvailable": true,
"audioEndpoints": { "orgWide": 1, "agentScoped": 0 },
"ready": true
}
}
| Field | Meaning |
|---|---|
destination | telenow (the default): recordings are kept here. send: each one is sent to your webhook and not kept. |
sendModeAvailable | Whether send can be switched on. While it is false, a PUT with send is refused with 409. Switching to telenow always works. |
audioEndpoints.orgWide | Enabled webhook endpoints with no agentId that name recording.audio in events. These receive every recording, softphone calls' included. For a sub-account, its parent account's org-wide endpoints count too: they receive the sub-account's recordings. |
audioEndpoints.agentScoped | Enabled endpoints scoped to one of your agents that name recording.audio. Each receives only that agent's recordings. |
ready | false only when destination is send and audioEndpoints.orgWide is 0. Recordings with nowhere to go are then lost, each reported as recording.failed with reason no_destination. |
An endpoint subscribed with ["*"] doesn't count: it never receives recording.audio, which goes only to endpoints that name it.
Change it
curl -X PUT https://api.telenow.ai/api/orgs/{orgId}/recordings/delivery \
-H "X-API-Key: $TELENOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "destination": "send" }'
The body has one field, destination: "telenow" or "send". The answer is the same object as the GET, after the change.
Before you switch to send:
- Create an endpoint with no
agentIdwhoseeventsincluderecording.audio. An endpoint scoped to one agent is not enough: it never receives other agents' recordings, or any softphone call's. - Make your receiver accept the
recording.audiorequest. It ismultipart/form-data, not JSON, and it carries the only copy of the audio.
Each recording follows the setting in force when it finishes, so a call already in progress when you switch follows the new choice. Recordings already kept in Telenow stay. Recordings sent while send was on were never kept, so switching back to telenow doesn't bring them here.
| Status | Error | When |
|---|---|---|
400 | `destination` is required: "telenow" or "send" | No destination. The body must be a JSON object with only that field: another field, or another value, is refused with 400 as well. |
403 | — | Your role, or the API key's, is not owner or admin. |
409 | Send mode isn't switched on for this deployment yet … | sendModeAvailable is false. |
409 | Add an org-wide webhook endpoint (no agent) that subscribes to `recording.audio` first … | No endpoint with no agentId names recording.audio. |
While recordings are sent to you
- Nothing is stored for new calls. They have no recording row, so the list,
/fileand/signed-urlendpoints have nothing for them, the dashboard can't play them, andrecording.readydoesn't fire. A recorded call'scall.endedsays"recordingMode": "send". - Uploads are refused with
409(see Uploading). - A transferred call's bridged part is not recorded. In this mode Telenow asks no carrier to record a transferred call, because the carrier would keep its own copy.
- A recording that can't be delivered is gone. Telenow retries it for a while from memory only, then reports it as
recording.failed. There is no copy to fetch later. - The last endpoint that receives every recording is protected. While
destinationissend, you can't takerecording.audiooff the last endpoint with noagentIdthat has it, or delete that endpoint: both are refused with409.
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. While recordings are sent to you instead, nothing is stored and recording.ready doesn't fire: the audio itself arrives as recording.audio.
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.