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

# Batch-ingest agent desktop screen recordings

> Buffer agent-side desktop capture locally and flush it in batches: align each screen recording with its call on the sync endpoint, optionally persist per-agent application-usage analytics, and keep consent and retention in the tenant's control.

# Batch-ingest agent desktop screen recordings

Quality reviewers in retail branch offices, remote sites on weak links, and regulated windows-VDI fleets all share the same problem: agents capture their desktops locally and only upload when the network allows. Two read-side surfaces under `/api/v1/recordings` ingest those buffered artefacts after the fact — screen recordings of the agent's desktop, and per-application usage segments — and bind them to the parent call recording so QA review, dispute evidence, and PCI attestation can replay what the agent saw.

Everything below uses `POST /api/v1/recordings/:id/...`, where `:id` accepts either the recording id or the call id of the interaction. That matters for batch upload: a desktop agent normally knows only the call id it captured during the live call, while a batch job reconciling new uploads on a schedule may hold either identifier. Both resolve to the same recording row.

## Why the batch path exists

Live video sessions (browser video rooms) stream their screen share in real time and are not what this is for. Desktop capture on contact-center agents runs against screen-tool APIs that only work deployed into each agent's workstation, and the upload is buffered on the agent's hard drive until connectivity allows — in supporting poor-connectivity branch sites (retail chains, rural shared services desks, offshore BPO partners on asymmetric links) this is not optional.

The ingest pipeline accepts artefacts post-hoc and resolves back to the interaction via the call id, so you can:

* Capture locally with whatever tool you already deploy — the agent-side collector is in your hands.
* Upload to cloud storage (your own staging bucket) and hand only the object path over the ingest API.
* Buffer on the workstation during an outage and flush the queue once the link recovers.

Batch structuring matters mostly for throughput: each flush POST resolves the call, derives a verdict, persists, and writes an audit event — skipped calls and out-of-bounds artefacts are still persisted as evidence, so tracking "which uploads already succeeded" is the lightweight idempotency check to build into the flusher.

## 1. Ingest screen recordings — sync evaluation + durable store

Screen recordings batch through `POST /api/v1/recordings/:id/screen-sync`. The endpoint evaluates whether the uploaded artefact actually covers the call it claims against a tolerance of 5 seconds on alignment and 90% on coverage, then persists the manifest, the verdict, and the consent/retention data onto the recording row under a dedicated slot.

The manifest the desktop collector posts:

```json theme={null}
{
  "gcs_path": "agents/desktops/ag-341/2026-08-29/call-CA4f2b91.mp4",
  "started_at": "2026-08-29T14:02:11Z",
  "ended_at": "2026-08-29T14:08:39Z",
  "duration_seconds": 388,
  "agent_id": "ag-341",
  "consent_method": "verbal_attested",
  "jurisdiction": "eu",
  "retention_days": 90,
  "redaction_windows": [
    {
      "start_at": "2026-08-29T14:04:02Z",
      "end_at": "2026-08-29T14:04:20Z",
      "reason": "card entry blanked (PCI scope)"
    }
  ]
}
```

Every call-recording window returns one of three verdicts:

* `synced` — all checks pass: an artefact was actually uploaded, its duration exceeds the 2-second floor, it started within 5 seconds of the call start, it overlaps at least 90% of the call window, and every PCI redaction window stays inside the artefact's own timeline.
* `out_of_sync` — one or more checks failed. The record is still persisted: "an agent uploaded a screen recording that did not line up" is itself dispute evidence, and the per-check report tells you exactly which clause broke.
* `skipped` — the call row has no finalized window yet (typically a still-open leg), so there is nothing to align against. The submission still persists, marked `skipped`.

Each record lands on the recording metadata under the `screen_recordings` key, re-submission of the same `gcs_path` updates in place (safe retry), and the per-recording array is capped at 20 — past that the API returns `SCREEN_SYNC_LIMIT_REACHED`. The full audit trail reads back with:

```
GET /api/v1/recordings/:id/screen-recordings
```

A failed upload does not mean "do not send": the recipient idempotency is the object path — batch jobs should POST the same `gcs_path` once and rely on an in-place update rather than deduplicate client-side.

Scope and roles match the recording-QC surface: `voice:read` to list, `voice:write` to ingest, and the ingest route requires an owner, admin, or developer role because it emits an auditable QM verdict.

## 2. Desktop analytics — per-agent application usage (optional)

If your collector also watches which application sits in the foreground (a standard capability of workstation capture agents built on OS focus APIs), you can attach quantitative application-usage analytics as well. The data ships through `POST /api/v1/recordings/:id/desktop-analytics`, and is independent of whether your tenant also records screens — pick one pipeline, the other, or both per agent profile.

The collector orders the observed foreground spans and posts:

```json theme={null}
{
  "agent_id": "ag-341",
  "segments": [
    {
      "application": "Devotel softphone",
      "category": "communication",
      "started_at": "2026-08-29T14:02:11Z",
      "ended_at": "2026-08-29T14:08:39Z"
    },
    {
      "application": "retail-crm",
      "category": "crm",
      "started_at": "2026-08-29T14:03:12Z",
      "ended_at": "2026-08-29T14:03:40Z",
      "window_title": "Order #4411 — Cards"
    },
    {
      "application": "spreadsheets",
      "category": "productivity",
      "started_at": "2026-08-29T14:03:41Z",
      "ended_at": "2026-08-29T14:04:02Z",
      "window_title": "pricing-lookup.xlsx"
    },
    {
      "application": "catalog-knowledge",
      "category": "knowledge",
      "started_at": "2026-08-29T14:05:20Z",
      "ended_at": "2026-08-29T14:06:11Z"
    }
  ]
}
```

The category vocabulary is closed: `communication`, `crm`, `productivity`, `knowledge`, `idle`, `other`. Labels decide which apps count as talk time vs. non-talk after-call work — idle spans drop out of the active-work totals entirely — so a bad label straight into `crm` or `productivity` skews your handle-time split upward rather than the report being wrong.

The report is derived entirely server-side from the segments; a client-sent report is ignored. The persisted record contains:

* `applications` — per application: cumulative foreground time, visit count, share of active desktop time, mean dwell per visit, and whether it tripped the automation-candidate heuristic (at least 3 revisits and 30 seconds of accrued visited data-handling categories).
* `top_transitions` — the highest-frequency application→application hop pairs, ranked (max 5 entries).
* `automation_opportunities` — apps worth an RPA look, each with the dwell/visit evidence.
* `non_talk_ratio` — the fraction of handle time spent outside the communication surface. This is the after-call-work signal.

Records persist keyed per agent; a second ingest for the same agent updates in place, and the maximum stands at 20 agents per interaction (warm-transfer chains). `GET /api/v1/recordings/:id/desktop-analytics` lists them, newest first.

Neither of these surfaces communicates with the carrier plane — these are read/QM/scoring artefacts over inbound interaction records, with no outbound contact.

## 3. Health and vitals reporting

Batch ingest failure modes surface in four predictable places, and the pattern for each:

* **Verdicts are evidence, not errors.** `out_of_sync` returns `200`. Treat it as an analytic signal for the QA dashboard, not a retry trigger. The `report.checks` array lists each clause (alignment over 5s, coverage under 90%, a PCI redaction window escaping the artefact's timeline, a sub-2-second artefact signalling a crashed collector) and the measured value, so drift triage is one aggregation away.
* **Ingest flushes with strongly-typed failures.** Invalid manifests return `400 SCREEN_SYNC_INVALID_BODY` / `400 DESKTOP_ANALYTICS_INVALID_BODY`; unknown recording ids return `404`. The coverage monotonic thing to aggregate is a counter of non-2xx per flush, and contents-specific counters on a per-`sync_status` basis.
* **Collector-side vitals piggyback on your own observability.** The agent workstation health signal (disk queue depth, oldest unsent artefact, consecutive sync-offset drift) belongs in whatever fleet health collector you already run — the ingest endpoints return enough to bind the flush's outcome to a workstation and does not share telemetry centrally.
* **`skipped` is a race against the call finalize.** Repeat the submission in the next batch window; the in-place upsert on `gcs_path` keeps replays safe, so the flush can be the naive "try unflushed records, then read back the list."

## 4. Consent, retention, and the tenant-owned flag

Agent-side capture of a screen is workplace-monitoring data, and Orbit's design preserves the tenant's authority over it: we do not gate capture on behalf of the tenant and the platform ingest only acts when your account enables it. Concretely:

* **The capture flag is yours, not ours.** Whether desktop capture runs at all is decided tenant-side on the workstation collector, typically behind a per-agent or per-team assignment flag. The ingest surfaces only receive what you enable them to receive; the flag lives with you. Orbit's compliance posture is intentionally tenant-owned: the platform supplies the controls and leaves policy decisions in your tenant configuration rather than deciding on your behalf.
* **Consent posture is recorded per artefact.** Send `consent_method` — the same closed vocabulary voice recordings use: `verbal_attested`, `operator_attested`, `ivr_dtmf`, `ivr_speech`, `consent_announcement`, or `none` — and optionally `jurisdiction`. Both carry through onto the audit record and the audit event.
* **Retention is explicit per artefact.** `retention_days` accepts 7–3650 (one week to ten years), matching the bounds voice recording configurations accept. Server-side this derives a `retain_until` timestamp on the record, so your retention sweep can expire desktop captures on the same schedule as audio. Leave it unset to inherit your tenant default.
* **PCI scope stays auditable.** Every `redaction_windows` entry the collector asserts (card-entry screens blanked by pause/blank) is validated to fall inside the artefact's own timeline — an invalid span will trip `out_of_sync` instead of silently undermining the PCI claim. Track the redaction windows on the same compliance review where you review call-recording redaction.
* **Storage and lifecycle mirror voice recordings.** Records travel with the parent recording row's tombstone, so a GDPR erasure of the interaction removes the screen-capture and desktop-analytics audit trails along with the audio.

## Record shape (`GET /:id/screen-recordings`)

Each item is a stable audit entry — id, artefact `gcs_path`, the timeline, `agent_id`, the consent/retention snapshot, the verdict snapshot (`sync_status`, `coverage_ratio`, `sync_offset_seconds`), and creation/update timestamps. Re-submission of the same `gcs_path` keeps the original id and `created_at` and moves `updated_at` forward — the audit identity is stable across retries.
