> ## 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.

# Troubleshooting: recording integrity, legal hold, and QC failures

> Recover from seal, verify, export-digest, legal-hold, QC, and share failures on recordings — read the error code, apply the matching recovery, and know when to retry versus escalate.

# Troubleshooting: recording integrity, legal hold, and QC failures

The recording pipeline fails closed: every endpoint returns a specific
`error.code` when something is wrong, and — like every Orbit error — the
response still arrives in the standard envelope:

```json theme={null}
{
  "error": {
    "code": "RECORDING_INTEGRITY_NOT_SEALED",
    "message": "This recording has no integrity seal yet. Seal it first.",
    "status": 400,
    "details": { "recording_id": "rec_01J8ZC3NPA" }
  },
  "meta": {
    "request_id": "req_9f3c…",
    "timestamp": "2026-09-07T10:12:00.000Z"
  }
}
```

Read `error.code` first, then match it to a section below. For the
happy-path calls this page troubleshoots, see
[Recording lifecycle operations](/guides/recording-lifecycle-operations) and
the [Recordings API reference](/api-reference/recordings).

***

## Symptom map

| Error code                                 | HTTP | What happened                                                                  |
| ------------------------------------------ | ---- | ------------------------------------------------------------------------------ |
| `RECORDING_INTEGRITY_NOT_SEALED`           | 400  | Export digest requested before the recording was ever sealed                   |
| `RECORDING_INTEGRITY_MISSING_CONTENT_HASH` | 400  | Seal attempted with no content hash available                                  |
| `RECORDING_SHARE_NOT_SHAREABLE`            | 400  | Share link minted for a recording that is not a completed video-room recording |
| `RECORDING_LEGAL_HOLD_SET_FAILED`          | 500  | Placing or releasing a legal hold threw after validation                       |
| `RECORDING_LEGAL_HOLD_FETCH_FAILED`        | 500  | Reading legal-hold state failed                                                |
| `RECORDING_QC_FETCH_FAILED`                | 500  | Reading the stored QC verdict failed                                           |
| `RECORDING_QC_RUN_FAILED`                  | 500  | The synchronous QC scorer threw or its write failed                            |
| `RECORDING_QC_INVALID_ID`                  | 400  | The recording id path param failed validation                                  |
| `RECORDING_INTEGRITY_SEAL_FAILED`          | 500  | Persisting the integrity seal failed unexpectedly                              |
| `RECORDING_INTEGRITY_VERIFY_FAILED`        | 500  | The recompute-and-compare verification threw before returning a verdict        |
| `RECORDING_INTEGRITY_EXPORT_DIGEST_FAILED` | 500  | Assembling or signing the export digest failed                                 |
| `RECORDING_SHARE_CREATE_FAILED`            | 500  | The share write or token mint failed after validation                          |

Codes that begin `RECORDING_CONSENT_` are a different class — see
[Consent-gate errors are tenant controls](#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:

```bash theme={null}
curl -X POST "https://orbit.devotel.io/api/v1/recordings/rec_01J8ZC3NPA/integrity/seal" \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

If the seal call itself fails with
`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.

**Fix.**

1. 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.
2. 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:

```bash theme={null}
curl -X POST "https://orbit.devotel.io/api/v1/recordings/rec_01J8ZC3NPA/integrity/seal" \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content_sha256": "0abc…​f9"}'
```

`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:

```bash theme={null}
curl "https://orbit.devotel.io/api/v1/recordings/rec_01J8ZC3NPA" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

For triage of a room whose recording never completed, work from
[Video room lifecycle](/troubleshooting/video-room-lifecycle) instead —
the recovery there is to run the room to completion, not to force the
share.

**Retry-safe:** retrying without a completed recording returns the same
400\. Share lifetimes clamp between 5 minutes and 30 days (7-day default)
— that is separate from this gate and applies only to a shareable
recording.

***

## `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.**

1. 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.
2. 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.
3. Include the recording id, the `error.code`, and the `meta.request_id`
   from the response in the ticket.

**Retry-safe:** one retry, then ticket. Never treat a 500 here as a cue
to keep hammering the endpoint.

***

## `RECORDING_QC_FETCH_FAILED` and `RECORDING_QC_RUN_FAILED` (500)

* `RECORDING_QC_FETCH_FAILED` — `GET /recordings/{id}/qc` could not load
  the stored QC verdict (a database or tenant-schema fault). This is
  distinct from a `pending` verdict, which is a normal 200 with an empty
  report.
* `RECORDING_QC_RUN_FAILED` — `POST /recordings/{id}/qc/run` threw while
  the synchronous scorer ran or while its verdict was persisted. A stuck
  `pending` verdict plus a rerun that keeps returning 500 means the
  scorer or its write path is failing, not that the recording failed a
  check.

**Fix.**

1. Retry `GET /recordings/{id}/qc` once; a read-only fault usually
   clears on a second attempt.
2. Re-run QC once more — the rerun is synchronous and idempotent, so a
   passed recording returns the same verdict and emits no webhook.
3. 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.

A verdict that comes back `failed` is not an error — triage it with the
`recording.qc_failed` webhook flow in
[Recording lifecycle operations](/guides/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 the `RECORDING_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.

These are tenant-owned controls: Orbit evaluates the gate you chose;
which jurisdiction rule you apply, and how you capture consent, is your
decision. Do not treat a consent 4xx as a platform-recovery problem —
the fix is configuration and evidence capture, not retry or escalation.
Go to [Call Recording Consent](/compliance/recording-consent) for the
consent models and acknowledgement flows, not to any section above.

***

## How a hash chain proves provenance

Sealing anchors one recording's content hash into a tenant-wide chain:
each seal stores a `prev_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](/guides/recording-lifecycle-operations)
§ Integrity seals and
[The export families model](/concepts/export-families-model).

***

## Error samples for tickets

Each sample below is a paste-ready response — include the whole block
(plus the `meta.request_id`) when you open a ticket.

Seal before finalize released the hash:

```json theme={null}
{
  "error": {
    "code": "RECORDING_INTEGRITY_MISSING_CONTENT_HASH",
    "message": "The recording has no content hash yet. Wait for finalize or pass content_sha256.",
    "status": 400,
    "details": { "recording_id": "rec_01J8ZC3NPA" }
  },
  "meta": { "request_id": "req_e41f…", "timestamp": "2026-09-07T10:41:03.000Z" }
}
```

Export digest on an unsealed recording:

```json theme={null}
{
  "error": {
    "code": "RECORDING_INTEGRITY_NOT_SEALED",
    "message": "This recording has no integrity seal yet. Seal it first.",
    "status": 400,
    "details": { "recording_id": "rec_01J8ZC3NPA" }
  },
  "meta": { "request_id": "req_9f3c…", "timestamp": "2026-09-07T10:44:12.000Z" }
}
```

Share minted on an incomplete video recording:

```json theme={null}
{
  "error": {
    "code": "RECORDING_SHARE_NOT_SHAREABLE",
    "message": "Only completed video-room recordings can be shared.",
    "status": 400,
    "details": { "recording_id": "rec_01J8ZC3NPA" }
  },
  "meta": { "request_id": "req_7a2d…", "timestamp": "2026-09-07T10:51:27.000Z" }
}
```

Legal-hold write that threw:

```json theme={null}
{
  "error": {
    "code": "RECORDING_LEGAL_HOLD_SET_FAILED",
    "message": "We couldn't update the legal-hold state for this recording.",
    "status": 500,
    "details": { "recording_id": "rec_01J8ZC3NPA" }
  },
  "meta": { "request_id": "req_3c88…", "timestamp": "2026-09-07T10:58:44.000Z" }
}
```

QC verdict read that threw:

```json theme={null}
{
  "error": {
    "code": "RECORDING_QC_FETCH_FAILED",
    "message": "We couldn't load the QC verdict for this recording.",
    "status": 500,
    "details": { "recording_id": "rec_01J8ZC3NPA" }
  },
  "meta": { "request_id": "req_a91b…", "timestamp": "2026-09-07T11:02:19.000Z" }
}
```

Pre-recording consent gate (tenant control, not a platform fault):

```json theme={null}
{
  "error": {
    "code": "RECORDING_CONSENT_REQUIRED",
    "message": "Recording consent is required before this call can be recorded.",
    "status": 422,
    "details": { "reason": "consent_not_acknowledged" }
  },
  "meta": { "request_id": "req_d60e…", "timestamp": "2026-09-07T11:09:37.000Z" }
}
```

***

## Retry safety summary

| Code                                         | Safe to retry?                          | Correct re-do                                               |
| -------------------------------------------- | --------------------------------------- | ----------------------------------------------------------- |
| `RECORDING_INTEGRITY_NOT_SEALED`             | After a successful seal                 | Seal, then same GET                                         |
| `RECORDING_INTEGRITY_MISSING_CONTENT_HASH`   | Yes                                     | Wait for finalize, or POST with `content_sha256`            |
| `RECORDING_SHARE_NOT_SHAREABLE`              | No effect until the recording completes | Check status, then share                                    |
| `RECORDING_LEGAL_HOLD_SET_FAILED`            | Once, then ticket                       | Same PUT                                                    |
| `RECORDING_LEGAL_HOLD_FETCH_FAILED`          | Once, then ticket                       | Same GET                                                    |
| `RECORDING_QC_FETCH_FAILED`                  | Once                                    | Same GET                                                    |
| `RECORDING_QC_RUN_FAILED`                    | Once, then escalate                     | Same POST                                                   |
| `RECORDING_QC_INVALID_ID`                    | No — fix the id first                   | Correct id, then the call                                   |
| `RECORDING_INTEGRITY_SEAL_FAILED`            | Once                                    | Same POST                                                   |
| `RECORDING_INTEGRITY_VERIFY_FAILED`          | Once                                    | Same GET                                                    |
| `RECORDING_INTEGRITY_EXPORT_DIGEST_FAILED`   | Once                                    | Same GET                                                    |
| `RECORDING_SHARE_CREATE_FAILED`              | Once                                    | Same POST                                                   |
| `RECORDING_CONSENT_REQUIRED` / `..._INVALID` | No — tenant configuration               | See [Call Recording Consent](/compliance/recording-consent) |

***

## 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 the `error.code` from the response.
* The `meta.request_id` from the failing response.
* For a share or seal rejection, the recording's status at the time of
  the call.

<Note>
  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.
</Note>

***

## Related references

* [Recording lifecycle operations](/guides/recording-lifecycle-operations) —
  the QC, legal-hold, seal, and share flows this page troubleshoots.
* [Recordings API reference](/api-reference/recordings) — endpoint shapes
  and scopes.
* [Error codes](/reference/error-codes) — the full registry entry for
  each code above.
* [Call Recording Consent](/compliance/recording-consent) — the consent
  gates the `RECORDING_CONSENT_*` codes enforce.
* [Video room lifecycle](/troubleshooting/video-room-lifecycle) — when a
  recording never completes and blocks sharing.
* [The export families model](/concepts/export-families-model) — where the
  signed export digest fits among export paths.
