Operate the post-call recordings pipeline
When a call or video room ends, the recording goes through a finalize step and lands in your recording library. Everything after that point — scoring the recording’s quality, preserving it for litigation, proving it has not been tampered with, exporting its transcript, and cutting shareable clips — is a set of tenant-owned controls on the /api/v1/recordings surface. This guide walks the whole pipeline in the order a regulated call-center operation usually runs it.
Every endpoint below takes a recording ID from the recordings list (see the recordings API reference). All of them authenticate with your API key or dashboard session and require the voice:read scope for reads; write actions additionally require voice:write, and the compliance-governance actions (legal hold, integrity seal, QC re-run) are gated to owner/admin (QC re-run also allows developer). The share-link surface is the one exception: it governs video-room recordings and uses the video:read / video:write scopes.
The pipeline at a glance
1. Quality control on recordings
QC scores a finalized recording for capture defects: files too small to hold audio, durations too short to match the call, drift between the expected and actual length, and finalize latency. The post-call pipeline runs QC automatically when a recording finalizes; these endpoints let you read the verdict and re-run it.
Read the verdict
qc_status is one of pending (never scored), passed, failed, or skipped. A pending verdict returns 200 with an empty report — a 404 means the recording ID itself is unknown.
Re-run QC
Re-running is synchronous and idempotent: running it on a passed recording returns the same verdict and emits no webhook. On failure it persists the verdict and fans out a recording.qc_failed webhook to your configured endpoints — so treat that webhook as at-least-once and key your handler on (recording_id, qc_status).
A re-run requires voice:write and a developer, admin, or owner role, and it is audit-logged.
Triage with the recording.qc_failed webhook
Subscribe to recording.qc_failed on the Developers → Webhooks screen (event catalog at webhook events). A minimal handler that flags failed recordings for review:
The event fires only on failure — silence means a pass. Full consumer setup (signature verification, retries) is in the webhook consumer guide.
2. Legal hold on recordings
A legal hold exempts a recording from the age-based retention sweeps — both the media object and the database row — so it survives litigation preservation or a regulator’s request. This is the recording-side twin of the conversation hold documented in Legal holds on messaging conversations, which hands recordings off without a follow-up; this guide is that follow-up. Holds are tenant-owned governance controls: owner/admin role plus voice:write, and every change is audit-logged.
Per recording
The response returns legal_hold, reason, updated_by, and updated_at. To release, send "hold": false. The reason is optional and capped at 512 characters.
Batched by conversation
When a dispute covers a whole conversation, hold every linked recording — voice call legs and video-room sessions alike — in one write:
The aggregate read reports a tally:
A conversation that exists with no linked recordings yet returns a 0/0 tally rather than a 404, so you can place holds before the first recording finalizes.
Preservation is your control, not ours — and this page is not legal
advice. Whether a recording must be preserved, for how long, and who must
attest to it are your organization’s calls. Confirm the specifics with
qualified counsel.
3. Transcript export as VTT or SRT captions
Download a completed recording’s transcript as a caption file for review tooling, player subtitles, or eDiscovery bundles:
format accepts vtt (WebVTT, the default) or srt (SubRip). Export requires a finalized recording with a transcript — the API returns RECORDING_TRANSCRIPT_NOT_COMPLETED or RECORDING_TRANSCRIPT_NOT_AVAILABLE otherwise.
Pseudonymized export for eDiscovery
For a PII-safe pull, opt in with pseudonymize=true. Segments are scrubbed in memory before rendering — email, phone, and long-ID patterns become [EMAIL], [PHONE], and [ID] tokens — while timestamps and speaker labels stay accurate. The response tells you what was scrubbed in the X-Pseudonymized-Count header:
Raw output is the default; pseudonymization is strictly opt-in, so an export pipeline picks it explicitly per pull. Any value other than the literal pseudonymize=true is rejected with a 400. For a video (rather than purely textual) redaction layer, see the recording redaction concept page.
4. Integrity seals: proving a recording has not changed
The integrity surface anchors a recording’s content into a tamper-evident hash chain and stamps the encryption posture of your key management setup. Seal once, then verify on demand and ship a signed digest with any exported media.
Seal a recording
The body is optional: without a content_sha256, the seal uses the content hash the egress pipeline stamped at finalize. Pass content_sha256 (64-char hex) only if you hash the bytes yourself, and encryption_algorithm to record the at-rest algorithm. Sealing requires voice:write plus an owner or admin role, and is audit-logged. The response returns the seal:
Each seal links (prev_hash) into your tenant’s recording hash chain, so an auditor walking the chain can confirm nothing was inserted, removed, or reordered. If the recording has no content hash yet — finalize has not stamped one and you did not pass one — sealing fails with RECORDING_INTEGRITY_MISSING_CONTENT_HASH; pass the hash or wait for finalize.
Check state and verify
A verify response reports valid: true|false, a reason on failure, and the current_hash versus the recomputed expected_hash.
Ship the signed export digest to an auditor
The digest is a portable, HMAC-signed bundle (recording ID, seal record, jurisdiction, issue time) that an auditor verifies offline against the exported media — seal it before exporting, or the endpoint returns 400. Ship digest.json alongside the media file; the auditor does not need API access.
5. Clips and auto-detected highlights
Two ways to cut a recording down to the moments that matter: manual clips where you name the range, and automatic highlight detection that proposes ranges from the transcript for an operator to promote or dismiss.
Manual clips
The response (201) returns the clip with a media_fragment like #t=61.2,74.4 — a standard media-fragment seek string your player can append to the recording URL to jump straight to the clip window. List with GET /recordings/:id/clips, remove with DELETE /recordings/:id/clips/:clipId. Creating and removing clips is audit-logged, and a recording carries a bounded number of clips (the API returns RECORDING_CLIP_LIMIT_REACHED at the cap).
Auto-detected highlights
Detection scans the transcript and proposes moments — questions, action items, sentiment peaks, and speaker handoffs — for review instead of scrubbing the whole timeline by hand:
Detection is idempotent — re-running replaces the proposal set. Each moment carries kind, start_ms/end_ms, a score, the detection reason, and its own media_fragment. Detection needs a completed, transcribed recording and returns RECORDING_HIGHLIGHT_NOT_TRANSCRIBED when no transcript exists.
Review the proposals with GET /recordings/:id/highlights, then promote the moments worth keeping into real clips, optionally renaming them:
Dismiss the noise:
Promoted moments land on the same clip list as manual clips, so downstream review tooling sees one uniform surface.
6. Share tokens for external auditors
For video-room recordings, you can mint a time-limited public link a guest opens without an Orbit account — the standard way to hand an external auditor or counsel playback access:
The response returns a share_url, the bearer token, and expires_at. Lifetimes clamp between 5 minutes and 30 days; omit expires_in_seconds for the 7-day default. These endpoints sit under the video:write scope and only completed video-room recordings are shareable (RECORDING_SHARE_NOT_SHAREABLE otherwise).
Revoke every outstanding link at once:
Revocation invalidates prior links immediately; new links can be minted afterwards. Minting and revoking are audit-logged. Public playback still registers in view analytics, so you can confirm the auditor actually watched before closing the matter.
Troubleshooting
QC stays pending forever. QC runs when the recording finalizes and the finalize hook passes the row through the scorer. A stuck pending usually means the finalize never completed — check the recording’s status in the recording library first. If the row is finalized, re-run with POST /recordings/:id/qc/run; the request is safe to repeat because a passed verdict emits no webhook.
Legal hold versus retention sweep ordering. Place holds as soon as a matter is anticipated. Holds exempt recordings from age-based sweeps from the moment they stand, but they cannot resurrect a recording a sweep already deleted before the hold existed. Conversation-scoped GET returning 0/0 is fine — hold it anyway; the batch write re-checks parent links per row and skips tombstoned rows.
Pseudonymize is rejected or the header is missing. Only the literal pseudonymize=true opts in — 1, yes, or on return 400 with RECORDING_TRANSCRIPT_INVALID_FORMAT. The X-Pseudonymized-Count header appears only on a pseudonymized pull; a raw export never sets it. If your gateway strips headers, check Content-Disposition for the transcript-pseudonymized- filename prefix as a fallback signal.
Sealing fails with a missing content hash. The egress pipeline stamps the content hash at finalize; sealing before finalize (or after a failed finalize) returns RECORDING_INTEGRITY_MISSING_CONTENT_HASH. Wait for finalize, or compute the SHA-256 of the media yourself and pass it as content_sha256.
Verify reports the seal is invalid. Treat a failed verify as a genuine tamper signal: the stored seal no longer matches the recomputed hash. Quarantine the recording and investigate before exporting; do not attach a failed-verify recording to an eDiscovery bundle.
Clip creation fails with a range error. end_ms must exceed start_ms, both must land inside the recording’s duration, and a clip must clear the minimum duration. RECORDING_CLIP_INVALID_RANGE returns the specific reason in the error details.