Start with state
Never read the raw status. recording.state is the one field that answers “is
there anything to play”, and it is served on both the calls list and the call
detail.
available is the only playable state, and it is the only one for which a
signed link is ever minted.
Beside state, both recording and screen_recording carry started_at,
duration_s (the container’s own duration, so it matches the scrub bar) and
bytes. expires_at says when retention will erase this call’s content, and is
null while retention is unlimited — which is the default.
Playing and downloading
GET /v1/calls/{session_id}/recording responds 302 with a redirect to object
storage. Every HTTP client follows it by default, so what you actually receive
is the audio.
The link expires in one hour. It supports Range, which is what lets a
player seek without pulling the whole call first — and why a browser re-requests
the same URL on every seek.
Pass download=true for a link the bucket serves as an attachment named
call-{session_id}.ogg. It has to be a separate link because the content
disposition is signed into the URL, and an <a download> cannot rename a
cross-origin file.
Both are minted on the call detail only, and only while the state is
available. A list of 200 calls does not carry 200 signed URLs that all die in
an hour, so url, download_url and expires_at read null in a list row.
What is in the file
One stereo Ogg/Opus file per call: the caller is the left channel and the agent is the right. Splitting the channels gives you either side alone — oneffmpeg -map_channel away — which is what makes the file useful both for
listening and for building training data.
It is what the agent heard after noise cancellation, not a tap on the phone
line. It does not include background audio, which the agent publishes as its own
track.
A call parked on hold during a transfer is silent for that stretch, and that
silence is inside the duration, inside the file and inside the bill. The
recording measures our part of the call: after a transfer connects, our
agent has left, and whatever the caller and the person said next is not here.
Screen recordings
A second, separate artifact — never muxed into the audio, whose file means one thing.
There is no screen recording on a phone call — a phone has no screen to share.
Deleting
DELETE /v1/calls/{session_id}/recording destroys the media and keeps
everything else. The transcript, the tool calls, the cost and the analysis stay
exactly as they are.
It deletes both artifacts together — the audio and, where there is one, the
screen video. They are one thing to the person clicking Delete.
It is quiet on anything that is not available, which is more than “already
deleted”: there is no object behind none, not_shared, failed or
consent_withdrawn, and each of those already says something truer than
deleted would. Overwriting consent_withdrawn in particular would destroy a
compliance record — that the caller asked — and replace it with somebody’s
click. expired is the same case for your own retention policy.
The one gap is pending: nothing is stored to delete yet. Ask again once the
call has settled.
DELETE /v1/calls/{session_id}, covered in
calls.
Retention deletes them for you
Your workspace’s retention policy purges recordings on a schedule, without anybody pressing anything, and the state becomesexpired. The default is
unlimited — a recording is kept until something deletes it. Nothing ages a file
out behind the database’s back, so state and reality cannot disagree.
recording.expires_at tells you the deadline for a given call. It is computed
once, when the call ends, from the policy as it stood then: changing
retention_days never moves a purge that is already scheduled. See
data retention and privacy.
When a link 404s
Asking for a recording that is notavailable returns 404 with a message
saying which state it is in — five different reasons for “no recording” is the
difference between understanding your own settings and filing a ticket.
Reading
recording.state off the call first answers the same question without
the round trip.
The recording.ready webhook
Audio reaching storage fires a recording.ready event carrying a signed url,
its url_expires_at, the duration and the size — so a consumer that wants its
own copy can take one without calling back. It fires for the audio only; a
screen recording produces no event of its own.
The link in that payload expires in an hour like every other. See
webhooks.
Recording settings
Turning recording on and off, consent disclosure, and the stop-recording tool.
Data retention
What is stored, for how long, and what a purge destroys.