> ## 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 view analytics: instrument a player for viewership and engagement reporting

> Report playback events from your video-room recording player with an opaque viewer id, watch the live unique/concurrent badge, and read the engagement report with completion rate and an audience-retention curve.

# Recording view analytics

When you share a completed video-room recording — for training, compliance, or a customer handoff — raw "link opened" counters tell you nothing useful. View analytics measure how the recording was actually watched: who watched, how far each viewer got, where the audience dropped off, and how many people are watching right now.

It works like any viewership pipeline: your player widget reports lightweight playback events as a viewer watches, and Orbit rolls those events up into a live badge and a post-hoc engagement report. This guide shows you how to build the emit pattern on a player you own (an embedded widget, a portal page, or an internal tool) and how to read the results.

<Note>
  This covers engagement on finalized video-room recordings — "who watched and how far." It is distinct from [session replay](/guides/session-replay), which captures DOM-level interactions on a web page. The two solve different problems and you can use both independently.
</Note>

## Playback ingest

Your player reports one event at a time to the ingest endpoint:

`POST /api/v1/recordings/{id}/views`

Each event carries the viewer's current head position, the total recording duration, and the watch-time accrued since the last event. The same response returns the live unique-viewer and watching-now counts, so the player can render a "watching now" badge without a second call.

| Field                   | Type   | Meaning                                                                      |
| ----------------------- | ------ | ---------------------------------------------------------------------------- |
| `viewer_id`             | string | An opaque, player-minted session id. Never an email, name, or other PII.     |
| `event_type`            | enum   | One of `play`, `pause`, `seek`, `progress`, `ended`. Defaults to `progress`. |
| `position_seconds`      | number | Current playback head position, in seconds.                                  |
| `duration_seconds`      | number | Total recording duration as your player knows it, in seconds.                |
| `watched_delta_seconds` | number | Watch-time accrued since the previous event, in seconds.                     |

Response (in the envelope's `data` object):

| Field                       | Purpose                                                                    |
| --------------------------- | -------------------------------------------------------------------------- |
| `accepted`                  | `true` when the event was stored.                                          |
| `recording_id`              | Echo of the recording id.                                                  |
| `unique_viewers`            | Distinct viewers over the retention window.                                |
| `concurrent_viewers`        | Viewers who reported an event within the last 60 seconds — "watching now." |
| `concurrent_window_seconds` | The size of the watching-now window (60).                                  |

Auth: an API key (`X-API-Key`) or a session JWT with the `video:write` scope. Values are clamped server-side, so a buggy player build (NaN, negative, or runaway deltas) cannot poison the aggregate; only grossly malformed payloads get a 400.

## Live monitor

The monitor answers "how many people have watched this recording, and how many are watching it right now":

`GET /api/v1/recordings/{id}/views`

| Field                       | Purpose                                                                                                      |
| --------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `unique_viewers`            | Distinct viewers over the retention window (the unique-viewer roster ages out 30 days after the last event). |
| `concurrent_viewers`        | Viewers whose last event landed within the last 60 seconds.                                                  |
| `concurrent_window_seconds` | The watching-now window that count is measured over (60).                                                    |

Poll it on whatever cadence your dashboard or badge needs — it is a cheap read requiring the `video:read` scope. The ingest POST already returns these two counts, so a player-side badge usually needs no live-endpoint polling at all.

## Engagement report

The post-hoc report rolls everything up — the Wistia/Vimeo-style engagement graph for operators deciding whether the shared recording works:

`GET /api/v1/recordings/{id}/views/report`

| Field                                       | Purpose                                                                                                                                                                                                                                                                      |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unique_viewers`                            | Viewers who reported at least one event.                                                                                                                                                                                                                                     |
| `total_watch_seconds` / `avg_watch_seconds` | Aggregate and per-viewer watch-time, in seconds.                                                                                                                                                                                                                             |
| `completed_viewers` / `completion_rate`     | Viewers who reached 95% of the recorded duration (players rarely hit exactly 100%), and that count as a \[0, 1] fraction.                                                                                                                                                    |
| `avg_completion_ratio`                      | Average per-viewer completion ratio in \[0, 1].                                                                                                                                                                                                                              |
| `total_events`                              | Events aggregated across all viewers.                                                                                                                                                                                                                                        |
| `first_viewed_at` / `last_viewed_at`        | Span over which the recording was watched (null before any event).                                                                                                                                                                                                           |
| `retention_curve`                           | Ten decile points: `viewers_reached` counts how many viewers got at least `upto_percent` of the way; `reached_ratio` is that count over unique viewers — the drop-off curve. Read it left to right: the sharpest percentage-point drop flags the moment the audience leaves. |
| `viewers`                                   | A per-viewer breakdown (furthest position, watch-time, completion ratio, completed flag, first/last seen), longest-watch first, capped at 500 rows — the aggregate stays the headline, the breakdown stays bounded.                                                          |

Read scope: `video:read`. Use the curve to find drop-off moments, the per-viewer rows to see who actually finished, and the completion rate as the single health number.

## Player integration recipe

Emit one event per player action with an opaque, player-minted `viewer_id` — never an email, name, or account id. Send the `play`/`pause`/`seek`/`ended` events as they happen, and a heartbeat `progress` event every \~15 seconds while playing so the live badge stays warm. Measure `watched_delta_seconds` locally between emissions, and debounce high-frequency actions (scrubbing, rapid seek/restore-back) so a scrub gesture sends one `seek` event, not fifty.

```javascript theme={null}
const video = document.querySelector("video");
const API = "https://api.orbit.devotel.io";
const REC_ID = video.dataset.recordingId;

// Any random string qualifies; regenerate per session, never derive it from
// an email, IP, or account id — it must carry no PII.
const viewerId = crypto.randomUUID();
let lastSentAt = 0;

function emit(type) {
  const now = performance.now();
  const delta = Math.max(0, (now - lastSentAt) / 1000);
  lastSentAt = now;
  fetch(`${API}/api/v1/recordings/${REC_ID}/views`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": "<your-video-write-key>",
    },
    body: JSON.stringify({
      viewer_id: viewerId,
      event_type: type,
      position_seconds: video.currentTime,
      duration_seconds: video.duration,
      watched_delta_seconds: video.paused ? 0 : delta,
    }),
  }).then((r) => {
    if (r.ok) return r.json();       // use unique/concurrent counts for a badge
    if (r.status === 503) return null; // analytics temporarily unavailable — keep playing
  }).catch(() => {});                 // never let analytics break playback
}

video.addEventListener("play", () => emit("play"));
video.addEventListener("pause", () => emit("pause"));
video.addEventListener("seeked", () => emit("seek"));
video.addEventListener("ended", () => emit("ended"));
setInterval(() => { if (!video.paused) emit("progress"); }, 15_000);
```

### Failure behaviour is fail-open

View analytics are best-effort by design. If the analytics backend is unavailable, the ingest and read endpoints answer `503` (`SERVICE_UNAVAILABLE`) — never `500` — and nothing else about playback changes. Treat a 503 as "skip this event and keep playing," exactly like the snippet above does. A player that gates rendering on a badge read should hide the badge, not the player, on a 503.

## Limitations

* **Scope**: the endpoint family covers finalized video-room recording artefacts only — the recording id comes from the video-room recording lifecycle. Other recording families are out of scope for this surface.
* **Tenant namespacing**: all viewership state is namespaced by your organization, so a caller can only ever read their own tenant's metrics.
* **Retention**: a recording's viewership aggregate ages out 30 days after the last reported event, matching the maximum share-link lifetime.
* **No PII by design**: the opaque `viewer_id` means the analytics carry no personal data — keep it that way and mint a fresh random id per session.

## Example: read the report

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/recordings/rec_abc123/views/report \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "data": {
    "recording_id": "rec_abc123",
    "unique_viewers": 42,
    "total_watch_seconds": 18320,
    "avg_watch_seconds": 436,
    "completed_viewers": 18,
    "completion_rate": 0.43,
    "avg_completion_ratio": 0.61,
    "retention_curve": [
      { "upto_percent": 10, "viewers_reached": 40, "reached_ratio": 0.95 },
      { "upto_percent": 20, "viewers_reached": 35, "reached_ratio": 0.83 }
    ]
  }
}
```

A `completed_viewers` of 18 out of 42 unique viewers is a 43% completion rate; the retention curve says 5% of the audience dropped before 10% and another 12% by the 20% mark, so the opening minutes lose the most viewers.

## See also

* [Recordings API](/api-reference/recordings) — legal holds, integrity seals, and share links for finalized recordings
* [Video channel](/channels/video) — how video-room recordings are produced
* [Session replay](/guides/session-replay) — DOM-level session capture, a different problem from recording engagement
