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

MethodPathPurpose
GET/api/orgs/{orgId}/recordingsList recordings (optional ?agentId=)
GET/api/orgs/{orgId}/recordings/{id}Recording metadata
GET/api/orgs/{orgId}/recordings/{id}/signed-urlGet a short‑lived download URL
GET/api/orgs/{orgId}/recordings/{id}/fileStream the audio (or redirect to storage)
POST/api/orgs/{orgId}/recordings/uploadUpload a recording (multipart)
GET/api/orgs/{orgId}/call-audio/{id}/fileAd‑blocker‑safe alias of …/recordings/{id}/file
GET/api/orgs/{orgId}/call-audio/{id}/signed-urlAd‑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.

PartRequiredDefaultNotes
fileyesThe audio bytes.
session_idnoLinks the recording to a call. Without it the row is orphaned and won't appear on any call detail.
agent_idnoEnables ?agentId= filtering on the list endpoint.
channelnowebweb, softphone, telephony.
mimenoaudio/wavSet it if you upload anything else, or players may refuse the file.
sample_ratenoHz, e.g. 24000.
duration_secnoSeconds. See the warning below.
partnoDistinguishes 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

StatusMeaning
201Created — returns the new recording row.
400Malformed multipart, or no file part.
403Key role is read‑only (viewer/member), or the key belongs to another org.
413Body over the 100 MB limit. Split long calls, or upload a compressed format.

Errors (all endpoints)

StatusMeaningFix
401Access token is requiredYou sent no credential, or a JWT without X-Org-Id.
401Invalid or revoked API keyCheck the key is complete — secrets are 52 characters and contain -/_, so a truncated paste looks like a revoked key.
403This API key is scoped to a different organization.The {orgId} in the path isn't the key's org.
404recording not foundWrong id, or it belongs to another org — the lookup is org‑scoped, so cross‑org ids read as "not found" rather than confirming existence.
500Something went wrongGeneric. 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.