The recordings screen lists recorded calls for the active org and plays them back in the browser. It is read-only: nothing on this screen starts, stops, or deletes a recording. The list does not come from a single table. One cdr.recordings procedure merges two sources — the cdrs table in ClickHouse and an object listing of the recording bucket — de-duplicates them by call id, then filters, sorts, and pages the merged set server-side.

What the list is built from

cdr.recordings runs two lookups in parallel for the requested org: Because a call can appear as several cdrs rows, the CDR side collapses them per call_id: max(starting_time), max(duration), max(recording_size_bytes), and argMax(..., starting_time) for caller, callee, direction, agent, and recording_url. You see one row per call, built from its latest CDR write. For every object that the listing returned, the procedure then looks the call id back up in cdrs for the same tenant to borrow call metadata. The merged map is keyed by call id, and the storage-derived entry is written last — so when a call exists in both sources, the row you see carries:
  • recording_url rewritten to s3://<managed-bucket>/<object-key>
  • recording_size_bytes taken from the object listing
  • everything else (participants, start time, duration, direction, agent) from the CDR lookup
This is why a row can show a managed-bucket URI even though the CDR recorded a different destination. It is also how a recording that exists in storage but has no CDR row at all still reaches the list — as long as it can be attributed to your tenant.

Tenant attribution and why some objects never appear

Objects from the listing carry a source. Objects whose source is shared live in a bucket that is not exclusive to one tenant, so the object key alone is not proof of ownership. For those, the CDR lookup is the attribution check:
  • Shared-bucket object with a matching cdrs row for this tenant — the row is included, using the CDR metadata.
  • Shared-bucket object with no cdrs row for this tenant — the row is dropped. It is not rendered greyed out, and it is not counted in total. TeleQuick would otherwise have no way to prove the audio belongs to the org asking for it, so the object is treated as invisible rather than exposed cross-tenant.
  • Object from a non-shared source — kept even with no CDR row, because the listing itself is already scoped to the org.
The practical consequence: if you know a recording exists in a shared bucket but it never shows up on this screen, the missing piece is the CDR row, not the audio. Check that the call wrote a cdrs row under this tenant with a matching call_id.

Reading a row with missing metadata

A storage-discovered row with no CDR metadata is populated from the object alone. Those rows look incomplete on purpose: So a row whose timestamp is minutes or hours after you expect is showing when the object was last written to the bucket. Treat the timestamp column as a call start time only for rows that have participants and a duration. One side effect matters for filtering: a row with no starting_time at all is treated as timestamp 0 by the range predicate, which puts it before any fromMs you set. Such a row only appears when the time range has no lower bound.

Filtering, search, and paging

All filtering happens on the server, over the merged set, before paging. The screens send filter state as query input and re-render whatever comes back — they do not post-filter rows in the browser. The surviving rows are sorted newest first by starting_time, then sliced by offset/limit. The response carries:
  • rows — the current page
  • total — the count of rows that passed the filters, which is what drives the pager and the “N recordings in range” subtitle
  • stats — total_recordings, total_duration, total_bytes, and avg_duration computed across the whole filtered set, not just the page
Because total is post-filter, widening the date range or clearing the search box changes the page count as well as the rows. The two screens drive the same procedure differently: Changing any filter resets the pager to the first page on both screens.

Filters the API supports but these screens do not send

cdr.recordings accepts an agent input — a case-insensitive substring match on the CDR agent attribution. Neither recordings screen exposes a control for it, so it is always unset from the console. Call the procedure directly if you need to narrow recordings to one agent. The Voice AI screen additionally leaves direction and search unset. If you need those, use the admin recordings screen or the procedure.

Playback and presigned download URLs

Nothing is fetched until you ask for it. Each row starts with a Load player / Play button; pressing it resolves that row’s recording_url into something the browser can play:
  • A URL that already starts with http:// or https:// (a bring-your-own destination) is used as-is.
  • An s3:// URI (the managed bucket) is exchanged for a presigned GET by calling storage.signedUrl({ orgId, callSid }). The procedure returns a url, and that URL is valid for one hour.
Playback is a native <audio controls> element pointed at the resolved URL. There is no HLS, no streaming session, and no request-header auth — the presigned URL is the credential. Resolved URLs are kept only in component state for the life of the screen. They are never persisted, because they expire. Reload the page and the row goes back to its unresolved state; press the button again to mint a fresh URL. Download works the same way. The admin screen resolves the URL, then triggers a browser download named <call_id>.wav. The Voice AI screen reuses the URL that playback already resolved as a plain download link, so it only offers download after you have loaded the player. Two things follow from the one-hour lifetime:
  • A download that starts near the end of the window can fail partway. Re-press the button to get a new URL.
  • A presigned URL you copy out of the page is a bearer credential for that recording until it expires. Treat it accordingly.

Enabling recording for a workspace

The admin recordings screen is gated on the features.recording entitlement flag for the active org. The gate is deliberately fail-open: To turn recording on for a workspace, move the org to a plan whose entitlement claims include features.recording. The flag controls this screen; it is not a per-call switch, and the screen itself has no toggle. Once the flag is set, recorded inbound and outbound calls appear here as the CDR pipeline and the bucket listing pick them up. The Voice AI recordings screen does not check the entitlement. It will render an empty list rather than an upsell panel for an org without the flag.

Troubleshooting an empty or short list