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, newest first (optional ?agentId=, ?sessionId=, ?limit=, ?cursor=)
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

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 — the data.nextCursor of 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.

PartRequiredDefaultNotes
fileyes—The audio bytes.
session_idno—Links the recording to a call. Without it the row is orphaned and won't appear on any call detail.
agent_idno—Enables ?agentId= filtering on the list endpoint.
channelnowebweb, softphone, telephony.
mimenoaudio/wavSet it if you upload anything else, or players may refuse the file.
sample_rateno—Hz, e.g. 24000.
duration_secno—Seconds. See the warning below.
partno—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

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.