Troubleshooting: recording integrity, legal hold, and QC failures
The recording pipeline fails closed: every endpoint returns a specificerror.code when something is wrong, and — like every Orbit error — the
response still arrives in the standard envelope:
error.code first, then match it to a section below. For the
happy-path calls this page troubleshoots, see
Recording lifecycle operations and
the Recordings API reference.
Symptom map
Codes that begin
RECORDING_CONSENT_ are a different class — see
Consent-gate errors are tenant controls.
RECORDING_INTEGRITY_NOT_SEALED (400)
You called GET /recordings/{id}/integrity/export-digest on a recording
that has no integrity seal. The digest is built from the stored seal
record, so an unsealed recording has nothing to sign and ship.
Fix. Seal the recording first, then re-issue the GET:
RECORDING_INTEGRITY_MISSING_CONTENT_HASH, handle that first (next
section), then come back here.
Retry-safe: no — repeat the GET only after a successful seal. A GET
that keeps returning 400 with a successful seal on record means you are
requesting a different recording id than the one you sealed.
RECORDING_INTEGRITY_MISSING_CONTENT_HASH (400)
The seal attempts to anchor a content hash, and none exists yet. Two
states produce this:
- Sealing before finalize — the egress pipeline stamps the content hash when the recording finalizes; a seal that lands earlier finds no hash.
- Sealing after a failed finalize — finalize did not complete, so no hash was ever stamped.
- Wait for the recording to reach a finalized state, then repeat the seal with an empty body — the seal picks up the hash the pipeline stamped. Check the recording’s status before sealing rather than polling blindly.
- If you have the media bytes already (e.g. you downloaded the file out-of-band), compute the SHA-256 yourself and pass it explicitly:
content_sha256 is a 64-character hex string. If finalize itself keeps
failing, seal with the verified hash and route the finalize failure to
your ingest path — do not seal against bytes you have not validated.
Retry-safe: yes, as a repeat POST with the same or corrected
content_sha256.
RECORDING_SHARE_NOT_SHAREABLE (400)
You called POST /recordings/{id}/share on a recording that is not a
completed video-room recording. Only completed video-room artefacts are
shareable; an in-progress room’s partial recording or a recording that
never completed cannot take a share link, by design.
Fix. Read the recording before you share. Fetch the recording (or
list the room’s recordings) and check its status; mint the share link
only once the recording is complete:
RECORDING_LEGAL_HOLD_SET_FAILED and RECORDING_LEGAL_HOLD_FETCH_FAILED (500)
A 500 on the legal-hold surface means the write or read itself threw
after the request passed validation, role checks, and scope checks — a
platform-side fault (a tenant-schema or database error), not a bad
request. SET covers PUT /recordings/{id}/legal-hold (place or
release); FETCH covers the GET that reads current hold state, including
the batched conversation-level read.
Fix.
- Retry the same call once — the common cause is a transient database fault, and a single re-issue usually lands the hold. The operation is per-recording idempotent, so a retry cannot double a hold.
- If the retry also returns 500, open a ticket and stop retrying. A legal hold is an evidence-preservation control: if acknowledging it is failing, the risk is that retention sweeps proceed against a recording you believe is held. Do not blind-rebuild the hold with repeated PUTs — capture the failing request and escalate.
- Include the recording id, the
error.code, and themeta.request_idfrom the response in the ticket.
RECORDING_QC_FETCH_FAILED and RECORDING_QC_RUN_FAILED (500)
RECORDING_QC_FETCH_FAILED—GET /recordings/{id}/qccould not load the stored QC verdict (a database or tenant-schema fault). This is distinct from apendingverdict, which is a normal 200 with an empty report.RECORDING_QC_RUN_FAILED—POST /recordings/{id}/qc/runthrew while the synchronous scorer ran or while its verdict was persisted. A stuckpendingverdict plus a rerun that keeps returning 500 means the scorer or its write path is failing, not that the recording failed a check.
- Retry
GET /recordings/{id}/qconce; a read-only fault usually clears on a second attempt. - Re-run QC once more — the rerun is synchronous and idempotent, so a passed recording returns the same verdict and emits no webhook.
- If the rerun keeps failing, escalate with the recording id and the
meta.request_id— the scorer is inside the post-call pipeline, and a persistent 500 is a platform fault your tenant cannot repair.
failed is not an error — triage it with the
recording.qc_failed webhook flow in
Recording lifecycle operations.
RECORDING_QC_INVALID_ID (400)
The :id path parameter on a QC endpoint failed validation — an empty,
oversized, or malformed recording id before any lookup ran.
Fix. Validate the id you interpolate into the URL: pull it from a
recording list or the webhook payload rather than a hand-built string.
Rerun with the corrected id. This code fires on QC-specific id checks;
other recording surfaces respond with their own ..._INVALID_ID or a
generic validation code — match the endpoint, not just the string.
Consent-gate errors are tenant controls
Codes in theRECORDING_CONSENT_* family are a different failure
class from everything above. A seal or legal-hold 500 is a platform
fault you escalate; a consent error is your own gate doing its job:
RECORDING_CONSENT_REQUIRED— the call could not start recording because a two-party or two-tier consent gate you configured had no acknowledgement on record. The gate stops the recording before any media is captured.RECORDING_CONSENT_INVALID— a consent receipt failed validation when you submitted it.RECORDING_CONSENT_ACKNOWLEDGEMENT_REQUIRED— an acknowledgement step your configuration requires was skipped.
How a hash chain proves provenance
Sealing anchors one recording’s content hash into a tenant-wide chain: each seal stores aprev_hash link to the seal before it, so an auditor
who walks the chain confirms nothing was inserted, removed, or
reordered. Verification recomputes the digest over the recorded content
hash, the previous seal, and the seal metadata, and compares it to the
stored current_hash — a mismatch names exactly which seal broke. The
GET /recordings/{id}/integrity/export-digest HMAC-signed bundle is how
you ship that evidence to an auditor alongside exported media; the full
model (including where the export digest sits relative to the other
export families) is in
Recording lifecycle operations
§ Integrity seals and
The export families model.
Error samples for tickets
Each sample below is a paste-ready response — include the whole block (plus themeta.request_id) when you open a ticket.
Seal before finalize released the hash:
Retry safety summary
When to escalate
Escalate to support when the retry above does not clear a 500, or when any legal-hold write fails on a second attempt. Include:- The recording id (
rec_…) and theerror.codefrom the response. - The
meta.request_idfrom the failing response. - For a share or seal rejection, the recording’s status at the time of the call.
Legal holds and consent gates are tenant-owned controls: Orbit
stores the hold and evaluates the gates you configure, but preservation
policy, consent capture, and jurisdiction choice stay with you. Nothing
on this page is a claim of GDPR, CCPA, or other compliance.
Related references
- Recording lifecycle operations — the QC, legal-hold, seal, and share flows this page troubleshoots.
- Recordings API reference — endpoint shapes and scopes.
- Error codes — the full registry entry for each code above.
- Call Recording Consent — the consent
gates the
RECORDING_CONSENT_*codes enforce. - Video room lifecycle — when a recording never completes and blocks sharing.
- The export families model — where the signed export digest fits among export paths.