Skip to main content

Recording lifecycle

Every recordable interaction on Orbit — a SIP call, a conference, an AI voice agent, a browser-softphone session, a video room — can produce a recording: a media file plus a row that tracks it. That row carries two independent axes. One is the recording’s own state machine — capture, finalization, availability. The other is a post-finalize quality verdict your tenant’s own QC pipeline writes beside it. Conflating the two is the single most common integration mistake on this surface. This page keeps them separate. The per-payload webhook contract for every recording.* event lives in the webhook events reference; filtering and reading recordings across classes lives in the recording and transcript library and the recordings API. This page ties them together at the concept level without restating them.

What a recording is — and which plane created it

A recording is a captured media artifact (an audio or video file in object storage) plus a durable row with a stable recording_id, a classification saying which surface produced it, and the lifecycle state described below. Which subsystem captures the file depends on the media plane the interaction rode — see Media planes:
  • Carrier plane (SIP/PSTN) — inbound DID calls, outbound calls, and the SIPREC fork an operator toggles mid-call. Capture happens on the carrier plane as SIPREC (forked RTP written to a spool), and a separate uploader service ships the finished file to object storage and reports back. See The SIPREC uploader flow.
  • Orbit Media (SFU) — video rooms and WebRTC softphone sessions. Capture happens as server-side egress on the SFU. Video-room recordings dispatch on the video.recording.* event family; the lifecycle shape is the same.
Either way the result lands in one unified recordings surface: classification is one of sbc, ai_agent, browser_softphone, conference, or video_room (webhook payloads spell the same distinction with call, conference, ai_agent, browser_softphone, video_room), and a conference recording carries conference_id where a call recording carries call_id.

The state chain

A recording that succeeds end-to-end passes through: capturing → finalized → available Each step is owned by a different actor: Beside the happy path:
  • A capture can fail. A storage write failure, a codec mismatch, or a call that ended before any audio was captured turns the state machine terminal at failed, reported on the recording.failed event with a machine-readable reason.
  • A capture can pause and resume mid-call. Secure-payment sessions (masked keypad card capture) pause the fork for PCI-DSS descoping and resume automatically when the session ends; an operator can also pause and resume directly. A pause never changes the recording’s state — it stays capturing; the toggle is an event, not a state.

QC verdicts — a parallel pipeline, not a state

Recording QC is the post-finalize quality-evaluation pipeline your tenant configures: it inspects the finished artifact (missing or truncated audio, empty captures, codec or duration anomalies) and writes a verdict on the recording row. Crucially, the verdict is not a state in the recording lifecycle — it rides beside it: Consequences for an integration:
  1. pending is transient. A recording whose QC has not run yet (or is running) sits at pending. Never treat it as a pass or a fail — it resolves shortly after finalization and, once resolved, does not normally regress.
  2. skipped means QC did not evaluate this recording. The tenant pipeline was not configured to score this class or this artifact — a skipped verdict is not a degraded verdict.
  3. failed on QC is advisory, not authoritative. A QC-failed recording is still a finalized, available recording — the artifact exists and downloads. The verdict tells a reviewer the artifact has a quality problem (e.g. truncated audio), and it fires a dedicated recording.qc_failed event so a triage queue can wake without polling the library.
  4. Video rooms report the same verdict differently. The video event family breaks the verdict out as video.recording.degraded, fired alongside video.recording.completed, with { status, reasons } on the payload. The voice-family recording.qc_failed covers the unified recordings surface across every class.
The QC verdict is readable per recording (qc_status, qc_failed_count) on the recording library and filterable via its qc_status parameter — qc_status=failed is the QC-triage worklist.

The event vocabulary

Recording-related webhooks fall into three groups. Keep them separate and each group’s semantics follow directly. Lifecycle — the state machine advancing, keyed on recording_id: Mid-call fork toggles — operator and system actions against a live capture. They carry call_id and change no state on the recording row: Post-finalize side channels — tenant pipelines reacting to the finished artifact. None of them change the recording’s lifecycle state: Video rooms emit their own mirrors — video.recording.completed, video.recording.failed, video.recording.degraded, video.recording.transcript_ready — same lifecycle shape, room-scoped payloads.

Why recording.retention_deleted exists

Retention is a tenant-owned control: you set per-channel retention windows (how long recordings and message attachments stay in Orbit’s storage before hard-delete). When a recording’s window closes, the artifact is purged from object storage — permanently. Six to twenty-four hours before that purge, Orbit fires recording.retention_deleted. It is a pre-notification, not a post-fact audit line. The payload carries gcs_uri, a signed download_url valid until purge_scheduled_at, and the retention_policy that triggered it. The purpose is a last-grace mirror: if your compliance posture requires an archive in your own storage, this event is the guaranteed final chance to pull the artifact out before it is gone. Tenants that mirror continuously will still want this event — anything your continuous mirror missed gets one final, explicitly-signed retrieval window. Treat the download URL as single-purpose: it expires at purge_scheduled_at, and after the purge the recording row survives (metadata, duration, QC verdict) while the artifact does not.

The SIPREC uploader flow

On the carrier plane, finalization is a pipeline, not a single action. SIPREC writes the forked media to a spool volume as raw audio, and a dedicated uploader service moves it to object storage before the completion signal can fire:
  1. Watch — the uploader watches the spool directory and waits for each capture file to go stable (its size and modification time stop changing). A capture’s container headers are only finalized when the last media packet lands, so a file that exists is not yet a file you can ship.
  2. Process — per stable file, the uploader resolves its sidecar metadata (call id, timestamps, duration, channel count), falling back to the bare filename when the sidecar is absent.
  3. Upload — the file uploads to object storage with resumable semantics, so a long recording survives a transient network fault without restarting from byte zero. The tenant attribution rides the object metadata, not the callback payload.
  4. Callback — the uploader posts a signed completion callback to the API with the call id, object path, duration, and file size. Tenant resolution happens on the receiver side by call-id lookup, which is why a missing or stale tenant hint in the sidecar cannot misroute the recording.
Only after steps 2–4 succeed does recording.completed / call.recording.ready mean anything. Two consequences follow: an end-of-call is not a downloadable-recording guarantee (budget a short finalize window in any UI gated on “recording exists”), and a spool-side failure between step 1 and step 4 is exactly what recording.failed reports with its reason.

What to branch on

Branch integrations on machine-readable fields only:
  • recording_id — the join key across every event in a recording’s life. Lifecycle and post-finalize events carry it; reconcile by id, never by arrival order. Call-scoped events also carry call_id; conference recordings substitute conference_id.
  • Event type, grouped as above — lifecycle events mutate the recording’s state; pause/resume toggles do not; post-finalize side channels mutate tenant-pipeline data beside it.
  • qc_status on the row — read it from the recording library or recordings API for the verdict. It resolves asynchronously after finalization; pending means “not scored yet,” skipped means “not scored at all.”
  • reason on recording.failed — the machine-readable capture-failure cause. Keep failure-rate metrics per reason, not per generic failure event.
  • Retention pre-notification fields — on recording.retention_deleted: download_url (fetch before purge_scheduled_at) and retention_policy (which window closed).

Common pitfalls

  1. Treating a QC verdict as a recording state. qc_status lives on the row beside the lifecycle; a QC-failed recording is still available, and a QC-passed recording still gets purged at retention close. Gate download flows on the lifecycle state, gate triage queues on the verdict — never one on the other.
  2. Waiting for recording.completed without a finalize window. The completion event fires when the artifact is downloadable, not when the call or room ended — on SIPREC captures the uploader hand-off sits between the two. Poll or alert against “call ended but no completion after N minutes” if you need liveness there.
  3. Deduping by type. Lifecycle, toggles, and post-finalize events all repeat underneath a retrying delivery path. Dedupe by recording_id plus event type, and let a later event-superseding update overwrite rather than append.
  4. Treating QC as authoritative for the artifact’s existence. QC verdicts are advisory: a triage queue on recording.qc_failed catches artifacts worth a reviewer’s eyes. Blocking download or display on a failed verdict loses recordings that are imperfect but usable — for video rooms, video.recording.degraded carries per-check reasons so you can decide.
  5. Ignoring recording.retention_deleted until the purge. The signed download URL dies with the artifact at purge_scheduled_at. Mirroring after purge is not a recovery path — the artifact is gone.
  6. Assuming one completion signal per recording class. Call-scoped recordings fire call.recording.ready; conference-class recordings can’t (there is no call_id), which is why recording.completed exists as the classification-agnostic alias. Subscribe to both, or to recording.completed alone with classification as your routing key.
Once the state machine and the parallel QC axis are clear, the per-event payloads are in the webhook events reference, and the library filters are in Recording and transcript library.