Before you start
Use a server-side API key withrecordings:read. The scope is independent of
calls:read and only appears on the key-creation form once an organization
administrator has enabled recordings for your organization; see
Authentication and scopes for how to add the scope to a
new key.
List recordings
CallGET /recordings. Results cover every recording the organization has,
across both call-storage tables, ordered newest-first by recording time.
include=download_url to mint a signed URL for every row in the same
response — the right choice when you are about to fetch many recordings in
bulk and want to avoid one extra request per row. Omit it for a lighter,
metadata-only page. expires_in only means something alongside
include=download_url on this operation; sending it without include
returns 400 validation_error instead of being silently ignored, because a
setting that appears to apply but does not would be worse than an error.
Sync recordings completely
recorded_at is the call’s time, not the time its download_url-bearing row
became fetchable. A recording whose URL is written well after the call — a
repair job, a re-run, a manual fix — can be written after your sync cursor has
already moved past it, and incremental sync will never see it again. This is
a real, measured limitation, not a hypothetical one, so the contract has two
layers.
Incremental sync — best-effort
Track the timestamp of your last successful sync. On the next run, callGET /recordings?created_after=<last sync time minus 24 hours> and dedupe
results against what you already have by id. The 24-hour overlap absorbs
the normal case — on measured production data, a recording written more than
six hours after its call is vanishingly rare — but it does not absorb one
repaired long after the fact. Run this as often as you like; it is cheap.
Full reconciliation — the only guarantee of completeness
Periodically walk the full cursor with nocreated_after at all, deduping by
id against what you already have. This is the only way to guarantee you
have every recording, and it is inexpensive. Measured across our full
platform on 2026-09-01: 32,261 recordings divided by 100 per page is about
323 requests, comfortably under six minutes at 60 requests per minute, and
every one of those requests returns metadata only — no audio bytes. That
count is our total across every organization, not yours — what carries over
to your own organization is the ratio (about one request per 100 recordings
you have) and the fact that a full walk is metadata-only regardless of
scale. Run this daily or weekly depending on how much a missed recording
costs you. Do not rely on incremental sync alone and call it complete.
Fetch a URL only when you are about to use it
GET /recordings/{recording_id} always includes download_url — no
include needed. If you are building a player inside your own product,
request this URL when the listener presses play, not when the page listing
recordings first loads. The URL defaults to a 900-second lifetime (override
with expires_in, 60–3600 seconds); a page left open longer than that would
otherwise hold a value that silently stops working.
Revoking access does not invalidate an issued URL
This is inherent to how a signed URL works, not an oversight: the URL itself
is the credential, and nothing checks the originating key again once it has
been handed out. The residual window is exactly the
expires_in value the
URL was minted with — at most one hour, 15 minutes by default. A customer who
assumes revocation is instant for already-issued links will make the wrong
security decision; plan around the expiry, not the revocation.