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

# Video engagement analytics: read speaker, chapter, and room-usage metrics

> Read video engagement data — per-session speaker share, screen share and hand-raise counts, chapter themes across recordings, and the org-wide room-usage aggregate with the never-joined bucket.

# Video engagement analytics

A finished video session leaves behind more than a recording. Orbit collects
a per-participant engagement feed for every session, aggregates theme-level
chapter analytics across your recordings, and rolls up org-wide room usage —
sessions, room-minutes, peak participants, and the never-joined bucket. This
guide covers what each feed returns, how to read it, and which product
questions each one answers.

All routes live under `https://api.orbit.devotel.io/api/v1` and are scoped to
your tenant: you can only ever read engagement for your own sessions.

## 1. What the engagement feed returns

The engagement feed answers, for one realized session of one room: who spoke
and for how long, who held the floor, who shared their screen, and who
raised a hand. The host's meeting client buffers the active-speaker,
screen-share, and hand-raise signals during the call and sends one
finalized rollup per participant when the room ends — you read the result;
you never have to collect signals yourself.

The session-scoped read is:

```bash theme={null}
GET /api/v1/video/rooms/{room}/sessions/{sessionId}/engagement
```

It returns four blocks:

| Block                              | Shape                                                       | Answers                                  |
| ---------------------------------- | ----------------------------------------------------------- | ---------------------------------------- |
| `participants`                     | One row per participant, most-speaking first                | Who carried the meeting; who never spoke |
| `total_speaking_seconds`           | Sum of all speaking durations                               | How much of the meeting was talk         |
| `speaking_share` (per participant) | 0–1 fraction of total speaking                              | Balanced panel or one dominating voice   |
| `dominant_timeline`                | Merged `{ participant_identity, start_ms, end_ms }` windows | Who held the floor at each moment        |

Each participant row carries `participant_identity`, `display_name`,
`speaking_duration_seconds`, `speaking_share`,
`screen_share_duration_seconds`, `hand_raise_count`,
`dominant_segments` (that participant's floor-holding windows), and
`first_spoke_at` / `last_spoke_at`.

Three reading rules:

* `has_data: false` means the session predates engagement capture or never
  finalised a rollup — render an empty state, not a table of zeros.
* The participant list is paginated (`?limit=`, `?offset=`, `?after=` —
  default page 50, maximum 200). Page through with the opaque
  `page.next_cursor` as `?after=`; a stale or garbled cursor degrades to the
  first page rather than erroring. Session totals and the dominant timeline
  stay computed over the full roster on every page, so a paged read never
  understates them.
* Sessions predate capture gracefully: a 404 means the session id is not in
  your workspace; a 200 with `has_data: false` means the session exists but
  has no engagement rows yet.

## 2. Reading one session's engagement

Page through the participant list and read the aggregates:

```bash theme={null}
curl -s "https://api.orbit.devotel.io/api/v1/video/rooms/room_weekly_sync/sessions/sess_8af3c1/engagement?limit=50" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

```javascript theme={null}
async function readEngagement(room, sessionId) {
  const headers = { "X-API-Key": process.env.ORBIT_API_KEY };
  let after;
  const all = [];
  do {
    const url = new URL(
      `https://api.orbit.devotel.io/api/v1/video/rooms/${room}/sessions/${sessionId}/engagement`,
    );
    url.searchParams.set("limit", "200");
    if (after) url.searchParams.set("after", after);
    const res = await fetch(url, { headers });
    if (res.status === 404) throw new Error(`No such session: ${sessionId}`);
    const { data } = await res.json();
    if (!data.has_data) return { hasData: false, participants: [] };
    all.push(...data.participants);
    after = data.page.has_more ? data.page.next_cursor : undefined;
  } while (after);
  return { hasData: true, participants: all };
}
```

Sort is already most-speaking first, so the first page is your speaker
leaderboard. A `speaking_share` of 0.85 on row one tells you one voice
dominated; ten rows clustered between 0.05 and 0.15 reads as a balanced
panel. Row-level `hand_raise_count` and `screen_share_duration_seconds` flag
active participants who barely spoke — the audience questions and the demo
segments show up there.

The `dominant_timeline` overlay answers "who held the floor when": plot each
window as a block on the session's timeline and you get the host/segment
structure of a webinar — long `host` windows at the open, short panel
windows mid-call, a return to the host for the close.

## 3. Session pages vs the org-wide aggregate

Per-session engagement answers "what happened inside this meeting." The
org-wide aggregate answers "how is the organisation using video rooms." They
are two different lenses — do not try to derive one from the other.

```bash theme={null}
GET /api/v1/video/rooms-analytics/usage?from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z
```

`from` and `to` are ISO 8601 bounds; omit both and the window defaults to
the last 30 days. The response carries a `summary` plus a `daily` series
(one bucket per UTC day, capped at one year of buckets) for
sessions-over-time charts:

| `summary` field                                                          | Meaning                                                                        |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| `total_sessions`                                                         | Realized sessions in the window                                                |
| `total_room_minutes`                                                     | Wall-clock room time, minutes                                                  |
| `avg_peak_participants` / `max_peak_participants`                        | Average and busiest concurrent-participant peak                                |
| `total_join_events`                                                      | Joins (a rejoin counts again)                                                  |
| `recordings_started` / `recordings_succeeded` / `recording_success_rate` | Recording pipeline health (rate null when nothing was recorded)                |
| `unused_rooms`                                                           | Sessions where **no participant ever connected** — idle cleanup, not a failure |
| `error_sessions`                                                         | Sessions that terminated with an error — the genuine-crash metric              |

Read `unused_rooms` and `error_sessions` apart. A high `unused_rooms` count
means rooms were minted and never joined — wasted scheduled meetings, or an
embed creating rooms speculatively. It is a utilisation signal, never a
fault. `error_sessions` is the metric to page on: those rooms reached
participants and then failed. (The legacy alias `join_failures`, equal to
`unused_rooms`, stays on the wire for older readers; render
`unused_rooms`.)

## 4. Chapter analytics across recordings

Where engagement reads one session live, chapter analytics reads your
recordings at aggregate: the auto-chaptered themes on your recordings are
grouped over a window so you can see which topics recur and how long they
run.

```bash theme={null}
GET /api/v1/video/chapter-analytics?from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z
```

Each row is `{ theme, occurrence_count, avg_duration_ms, recordings_count }`
— a normalised chapter title, how many chapters carry it, their average
length, and how many distinct recordings contained it, most-frequent first,
capped at 50 themes. The window defaults to the last 30 days.

Use it to answer "what do our calls actually spend time on" — a rising
`Pricing objection` theme tells sales enablement what to train; themes that
only appear on one recording (`recordings_count: 1`) are one-offs, not
trends.

## 5. Mapping metrics to product questions

| Question                               | Feed                                             | Read                                                                                                                                                                                                          |
| -------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Which webinar segments held attention? | Session engagement `dominant_timeline`           | Long single-holder windows early vs fragmented windows late; pair with the playback-side retention curve in [Recording view analytics](/guides/recording-view-analytics) to see where replay viewers drop off |
| Which hosts hold the floor vs open it? | Session `speaking_share`                         | Host share near 1 means a monologue; share spread across panelists means the host facilitates                                                                                                                 |
| Who in the panel never got heard?      | Session `participants`                           | Rows with `speaking_duration_seconds: 0` but nonzero `hand_raise_count` — they tried                                                                                                                          |
| Did the demo actually happen?          | Session `screen_share_duration_seconds`          | Duration on the presenter's row                                                                                                                                                                               |
| Are scheduled rooms getting used?      | Org aggregate `unused_rooms` vs `total_sessions` | Rising unused share = minting rooms nobody joins                                                                                                                                                              |
| Is the recording pipeline healthy?     | Org aggregate `recording_success_rate`           | Null means nothing was recorded; below 1 means egress is dropping files                                                                                                                                       |
| Are we busy and when?                  | Org aggregate `daily` buckets                    | Sessions and room-minutes per UTC day                                                                                                                                                                         |
| What do our calls talk about?          | Chapter analytics `themes`                       | Most-frequent themes by occurrence; cross-sample with `recordings_count`                                                                                                                                      |

A sample dashboard read: chart the org aggregate's `daily.sessions` as a
sparkline, `summary.unused_rooms` as a stat next to `total_sessions` (a
ratio, not a raw count), `summary.recording_success_rate` as a gauge, and
the chapter themes as a top-N bar chart. For live "who is talking now"
during a call, use the in-call snapshot read covered in
[Monitor in-call video quality](/guides/video-quality-monitoring) — this
feed is for after the room ends.

## Worked example: a weekly engagement report

```javascript theme={null}
const headers = { "X-API-Key": process.env.ORBIT_API_KEY };
const base = "https://api.orbit.devotel.io/api/v1";

// 1. Find ended sessions (the same rows the History page renders).
const sessions = await fetch(
  `${base}/video/rooms-scheduled?status=ended`,
  { headers },
).then((r) => r.json());

// 2. Per session, read engagement and keep the speaker leaderboard.
const reports = [];
for (const s of sessions.data.slice(0, 20)) {
  const eng = await fetch(
    `${base}/video/rooms/${s.room_id}/sessions/${s.id}/engagement?limit=5`,
    { headers },
  ).then((r) => r.json());
  if (!eng.data.has_data) continue; // predates capture — skip, don't chart zeros
  reports.push({
    session: s.id,
    topSpeaker: eng.data.participants[0]?.display_name,
    topShare: eng.data.participants[0]?.speaking_share,
  });
}

// 3. Org-wide context for the same window.
const usage = await fetch(
  `${base}/video/rooms-analytics/usage`,
  { headers },
).then((r) => r.json());
console.log({
  sessions: usage.data.summary.total_sessions,
  unusedRooms: usage.data.summary.unused_rooms,
  reports,
});
```

Keep the poll size small (`?limit=5` here) when you only need the
leaderboard; page the full roster only when a report needs every
participant.

## What to check when something looks wrong

* **`has_data: false` on a recent session** — the rollup is sent at
  room-end; give it a moment, then re-read. Sessions that predate engagement
  capture never gain rows.
* **A 404** — the session id is wrong for this workspace, not "no data"; the
  two are deliberately different signals.
* **Participants missing from a long roster** — you are reading page one of
  a paginated list; follow `page.next_cursor`.
* **`recording_success_rate: null`** — nothing attempted a recording in the
  window, not a failure.
* **High `unused_rooms`** — usage signal, not an outage; look at how and
  when your rooms are created before treating it as a defect.

## Where to go next

* [Video room history](/guides/video-room-history) — the dashboard view over
  ended sessions, recordings, and transcripts that these reads back.
* [Recording view analytics](/guides/recording-view-analytics) — playback
  engagement for shared recordings: completion rate and the drop-off curve.
* [Monitor in-call video quality](/guides/video-quality-monitoring) — the
  live connection-quality snapshot for during the call.
* [Video API reference](/api-reference/endpoints/video) — the full endpoint
  surface, scopes, and parameter details.
