A session is one live conversation. A browser socket, a native client, one half of a duet and a live interpretation are all the same kind of row, so tooling you write against one works against all of them. In the console this is Studio → Sessions. Over the API it is GET /v1/speech/sessions.
Read this before the samples below. Recording is on — both switches that gate it ship on. SPEECH_SESSION_RECORDING is the deployment’s capability and defaults to true; record_sessions on a workspace is that workspace’s choice and also defaults to true. A session is recorded when both are on, so the samples here are what you get by default.Turning it off is a per-workspace decision, in the console under Settings, and it stops transcripts and audio for that workspace without touching anyone else.Recording voice carries consent obligations that differ by jurisdiction, and meeting them is the deployer’s duty, not the platform’s — every live session is handed the wording to show, before a single audio frame is relayed. See the disclosure event.A row is written for every session either way, so a finished conversation can always be found by id, model, source and duration. What is kept, and what is not has the rest.

Find a conversation

That is a session that was recorded, which is what ships. Where recording was turned off — by the deployment or by the workspace — the call still succeeds and every row looks like this instead:
turns and median_reply_ms are both derived from the transcript, so with no transcript there is nothing to derive: zero turns and a null median describe a conversation that happened and was not kept, not one that went badly. source is whatever the client passed on the query string when it opened the socket — live, duet, reader, ios, api. It is an optional parameter with no allowed-values list, so a client that omits it stores null, which is what a third-party client gets unless it opts in. paired_session_id is the other half of a duet, so both sides can be replayed together. median_reply_ms is a float. It is the median of the per-turn ttfb_ms values, so an even number of replies puts it on a half millisecond — compare it numerically rather than matching an integer.
The list takes limit (1 to 200, default 50) and offset, and nothing else. There is no server-side filter by model, source or date: page through and filter in your own code, using total to know how far the list goes. The list omits transcripts on purpose — fifty conversations of text to render five columns that show none of it.

Read one

GET /v1/speech/sessions/{session_id} returns the same object with transcript included.
Turns arrive in the order they happened, each { "role", "text", "at" }at being seconds from the start of the session, so a transcript lines up with the recording without a second clock:
ttfb_ms on an assistant turn is end-of-user-speech to first audio out — the number the person on the other end actually felt. median_reply_ms on the summary is the median of those. Watch the tail rather than the mean: slow turns are what make people talk over the model. See Latency. An interrupted turn was cut short by barge-in, and its text is trimmed to what the listener actually heard rather than what the model had generated. Without that trim a transcript claims things were said that nobody heard.

Download the audio

Audio is stored per direction, not mixed. The realtime protocol carries the two sides separately, so keeping them separate means you know who said what with no diarization step. The tracks are user and assistant. Each download mints a fresh presigned URL rather than proxying bytes through the API, which is why the storage bucket needs no anonymous access at all:
The URL is good for one hour. Ask for a track the session does not have and the 404 lists the ones it does.

What is kept, and what is not

A session recorded by neither switch keeps nothing but its row. Whether the deployment turned the capability off or the workspace turned its own setting off, the shape is the same: tracks is [], transcript_retention is null, turns is 0, median_reply_ms is null, and GET /v1/speech/sessions/{session_id} returns "transcript": []. The row itself is still there. Both switches ship on, so this is the shape of a workspace that chose it.
Three fields tell you what survived a session that was recorded: Transcript capture is bounded so one runaway session cannot grow without limit: 6,000 entries, 2 MiB of text, and 64 KiB per entry. A normal two-hour conversation fits inside all three. Past a limit, capture stops explicitly and transcript_retention.truncated is true with a reason — retained text is never sliced mid-word to make room. Audio can also be missing from a recorded session for a reason that is not your doing: if the recorder cannot reserve capacity or storage is unavailable, the transcript is still kept and the client was told at session start that recording was unavailable. See the recording disclosure.

Erase one

A real delete, not a flag: the audio objects come off first, then the transcript and the row. A recording is a person’s voice and a transcript is what they said, so “deleted” has to mean the bytes are gone. deleted_tracks is the number of storage keys the erasure covered, not the number of audio files that existed. The set unions whatever the row recorded with the two deterministic keys, user.wav and assistant.wav, so a session that was never recorded also deletes 2 and a row carrying historical keys can report more. Read it as “this many objects were accounted for”, not as a count of what was found. If a track fails to erase you get a 502 and the row is left in place, so the delete can be retried rather than half-succeeding in silence. Retry it; a second pass finds whatever is left. The same erasure path runs from the organization-wide sweep and from the retention schedule, so there is one definition of what “erased” covers. See Compliance for the schedule and what a data subject request maps to.

Retention

A session is kept until your organization has a retention period configured, and that period is not self-serve today — ask support to set it. Once set, one period governs realtime sessions, saved transcription runs and synthesis history alike, and a daily sweep erases anything older through exactly the path above. An organization that has never configured a period has no retention limit on recorded data. If your own privacy policy states one, that is the gap to close. Compliance has the full schedule, including the parts that are not configurable.

Access

These are organization-scoped reads: only the organization that owns a session can see it, whether you authenticate with an API key or a signed-in console session. A session id appears in storage keys, so possession of an id must not on its own grant access — every call above requires credentials, and a session belonging to another organization answers 404, identical to one that never existed.

Going further

Realtime interaction

The sessions these rows record.

Turn-taking

Diagnosing a bad conversation from its own record.

Compliance

Consent, disclosure and the retention schedule.

Errors

Classifying a failure in one lookup.