> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Recording lifecycle: capture to retention purge, and the states in between

> How a recording moves from capture start through finalization to availability, which plane captured it (SIPREC on the carrier plane vs SFU egress on Orbit Media), why QC verdicts are a parallel pipeline rather than a recording state, and why the retention pre-delete event exists — before you build a recording archive or QC triage integration.

# 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](/reference/webhook-events); filtering and reading recordings across classes lives in the [recording and transcript library](/voice/recording-library) and the [recordings API](/api-reference/quality). 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](/concepts/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](#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:

| Step        | Meaning                                                                                                                          | What advances it                                                                                                                                                                                              |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `capturing` | The fork or egress is live and media is accumulating.                                                                            | Started by `POST /api/v1/voice/calls/{id}/recording/start`, a per-call `record: true` flag, a conference-recording setting, or SFU egress start. It can pause and resume mid-call without leaving this state. |
| `finalized` | Capture stopped and the complete file landed in object storage. For SIPREC captures this is where the uploader hand-off happens. | The end of the call, conference, or room — or an explicit stop.                                                                                                                                               |
| `available` | The recording row now points at a downloadable artifact and joins the searchable library.                                        | The completion signal — `recording.completed` / `call.recording.ready`.                                                                                                                                       |

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:

| Field           | Values                                                         | Written when                                                 |
| --------------- | -------------------------------------------------------------- | ------------------------------------------------------------ |
| Recording state | `capturing` → `finalized` → `available` (or terminal `failed`) | By the capture path, above.                                  |
| `qc_status`     | `pending` · `passed` · `failed` · `skipped`                    | By the post-finalize QC pipeline, after the artifact exists. |

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](/voice/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`:

| Event                  | Fires when                                                                                                                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `recording.started`    | Capture begins (recording entered `capturing`).                                                                                                                                                                     |
| `recording.completed`  | Capture finalized; alias of `call.recording.ready` with the same payload — documented separately because conference recordings carry `conference_id` instead of `call_id`, where the call-scoped event can't apply. |
| `call.recording.ready` | Same finalization for a call-scoped recording: the file is downloadable (`recording_url`, `duration_seconds`).                                                                                                      |
| `recording.failed`     | Capture went terminal `failed`; the payload's `reason` is the machine-readable cause.                                                                                                                               |

**Mid-call fork toggles** — operator and system actions against a live capture. They carry `call_id` and change no state on the recording row:

| Event                    | Fires when                                                                                        |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| `call.recording.paused`  | The SIPREC fork was paused mid-call — by an operator or by a secure-payment session starting.     |
| `call.recording.resumed` | The fork resumed — after an operator resume, or automatically when a secure-payment session ends. |

**Post-finalize side channels** — tenant pipelines reacting to the finished artifact. None of them change the recording's lifecycle state:

| Event                             | Fires when                                                                                   |
| --------------------------------- | -------------------------------------------------------------------------------------------- |
| `recording.qc_failed`             | The post-finalize QC pipeline scored the artifact and the verdict is `failed`.               |
| `recording.chapters.ready`        | Auto-chaptering finished for the recording — chapter markers are available.                  |
| `recording.redaction_vault.ready` | The compliance redaction vault finished sealing a redacted copy of the recording.            |
| `recording.retention_deleted`     | The retention window closes on the artifact — a pre-notification fired before purge (below). |

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](/reference/webhook-events), and the library filters are in [Recording and transcript library](/voice/recording-library).
