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

# Supervisor live speech analytics stream

> Subscribe to real-time speech analytics for one live call as a supervisor: keyword and watchlist hits, talk-to-listen ratio, dead air, interruptions, caller stress and synthetic-voice risk, delivered over server-sent events.

# Supervisor live speech analytics stream

`GET /api/v1/voice/supervisor/calls/{callId}/analytics/stream` opens a server-sent events (SSE) connection that streams utterance-level speech analytics for one in-progress call to a supervisor. Use it to build listen-along dashboards, agent-assist pilots, and "flag walls" that surface the moments a supervisor should act on while the caller is still on the line.

## What supervised live speech analytics is

A supervisor watching one live call gets a ranked feed of utterance-level signals as each utterance settles:

* **Keyword hits** — a watch phrase from your speech-analytics taxonomy appeared in a settled utterance, with the speaker, a running occurrence count, and a short surrounding snippet.
* **Conversation metrics** — talk-to-listen ratio, agent talk share, cumulative dead air (total, event count, longest gap), and interruption counts, refreshed after every settled utterance.
* **Caller stress** — a real-time acoustic reading of the caller's voice (arousal, stress score, and a label from `silent` to `stressed`), present when acoustic emotion extraction is enabled on your deployment.
* **Synthetic-voice risk** — a live voice-clone risk score with an escalating recommended action (`monitor` to `fraud_route`), present when the synthetic-voice gate is enabled on your deployment.

This is distinct from the per-agent quality pipeline. [Quality evaluation](/concepts/quality-evaluation-lifecycle) scores completed calls in batch from stored recordings and transcripts and feeds scorecards, coaching, and trends. The live stream never waits for the call to end and never needs the recording: it folds the live transcript turn by turn, so a supervisor can intervene mid-call. The two surfaces share your keyword taxonomy but nothing else; turning one off does not affect the other.

## The endpoint and the stream contract

```bash theme={null}
curl -N "https://api.orbit.devotel.io/api/v1/voice/supervisor/calls/call_123/analytics/stream" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The response is `text/event-stream`. Every frame carries a monotonic `id`, an `event` name, and a JSON `data` payload; a `: ping` comment frame arrives every 15 seconds so proxies and clients can detect a dead connection.

A session always starts with two handshake frames:

```text theme={null}
id: 1
event: connected
data: {"callId":"call_123","status":"ringing"}

id: 2
event: status
data: {"status":"ringing","terminal":false}
```

If the call has already ended, the stream sends `event: end` with the reason `terminal` and closes; post-call analytics for finished calls live in the [quality evaluation](/concepts/quality-evaluation-lifecycle) surface instead. A single session also closes itself after 30 minutes with `event: end` and the reason `max-duration` — reconnect to keep watching.

Live signals arrive as `event: analytics` frames. The `data` payload is one of four kinds:

| `kind` | Payload | When it fires |
| - | - | - |
| `metrics` | `talk_ms` per speaker, `talk_listen_ratio`, `agent_talk_pct`, `final_turns`, `silence_total_ms`, `silence_events`, `longest_silence_ms`, `interruptions` | After every settled utterance |
| `keyword` | `phrase`, `speaker`, `occurrences`, `snippet` (capped at 160 characters) | When a settled utterance contains a watchlist phrase |
| `emotion` | `arousal`, `stress_score`, `label`, acoustic `features`, `confidence` | Per analysis window, when acoustic emotion extraction is enabled |
| `clone-risk` | `synthetic_probability`, `risk_score`, `risk_level`, `recommended_action`, `confidence` | Per analysis window, when the synthetic-voice gate is enabled |

```text theme={null}
id: 7
event: analytics
data: {"kind":"keyword","ts":1752580812000,"phrase":"cancel my contract","speaker":"customer","occurrences":2,"snippet":"…honestly I'd rather just cancel my contract at this point…"}

id: 8
event: analytics
data: {"kind":"metrics","ts":1752580812000,"metrics":{"talk_ms":{"agent":41200,"customer":26800,"other":0},"talk_listen_ratio":1.54,"agent_talk_pct":0.61,"final_turns":14,"silence_total_ms":9400,"silence_events":2,"longest_silence_ms":5200,"interruptions":{"agent":1,"customer":2,"total":3}}}
```

Metric semantics worth coding against:

* `talk_listen_ratio` is agent talk time divided by customer talk time, rounded to two decimals, and is `null` until the customer has spoken at least once. `agent_talk_pct` is the agent's share of agent-plus-customer talk time, 0 to 1, and likewise `null` before both parties have spoken.
* A gap of 3 seconds or more between one utterance ending and the next beginning counts as a dead-air event; `longest_silence_ms` tracks the worst one this call.
* An interruption is one speaker starting more than 400 ms before the previous speaker finishes; counts are kept per speaker plus a `total`.
* `keyword` events for one utterance always arrive before that utterance's `metrics` snapshot, so a UI can paint the flag and the updated tiles from a single batch.

## Authorisation — who may open the stream

The route requires an owner, admin, or supervisor in the workspace; every other role receives `403` before any streaming starts. Server-to-server clients authenticate with an API key that carries one of those roles; browser dashboards authenticate with the session of a signed-in supervisor.

On top of the role gate, the `supervisor` role is scoped per queue:

* Orbit looks up the queue the call landed in from the call's routing metadata. If the call went through a queue, the supervisor must be explicitly assigned to that queue, otherwise the request returns `403` with `Supervisor is not assigned to this queue.` The error deliberately does not reveal which queues exist or which queues you are assigned to.
* Owners and admins are never queue-scoped and can open the stream for any call in the workspace.
* A call that never entered a queue (for example a direct dial to an extension) has no queue to check; any supervisor in the workspace may open its stream. If team-scoped visibility matters for your deployment, route calls through queues.

A call that does not exist in your workspace returns `404`, an invalid `callId` returns `400`, and a missing or expired credential returns `401`.

## Watchlist resolution

The keyword watchlist is your tenant's speech-analytics taxonomy: the keywords you maintain in your quality categories, the same vocabulary that powers post-call keyword detection. It is tenant-scoped, not queue-scoped — every supervisor in the workspace watches the same list.

Resolution happens once when the stream opens. The list is normalised (lower-cased, trimmed, de-duplicated, empty entries dropped) and matching is case-insensitive substring matching against each settled utterance.

If your tenant has no keywords configured, the stream still opens and still delivers `metrics` (plus `emotion` and `clone-risk` where enabled) — only the `keyword` events stay silent. Configure keywords in your quality categories first, then open the stream; changes to the taxonomy apply to streams opened afterwards.

## Live redaction of analytics frames

If your tenant has transcript redaction enabled in its compliance settings, the same policy that masks spoken card numbers, social security numbers, and contact details in the live transcript stream also applies here. Redaction runs before any keyword snippet is built, so a spoken card number can never appear in a `snippet` on this stream, masked on one surface and raw on the other. Metrics carry no text and are unaffected. If redaction is off for your tenant, snippets carry the utterance text verbatim, exactly as the transcript stream does.

## Example — a supervisor flag wall

A minimal web component that subscribes to the stream and renders each signal as a chip, with a filter over your keyword categories. It assumes the dashboard is served to a signed-in supervisor whose session the browser sends with the request; for server-side consumption, read the same stream with `fetch` and an `X-API-Key` header instead.

```html theme={null}
<flag-wall call-id="call_123"></flag-wall>

<script type="module">
  // Your quality categories, as configured in the workspace. The stream
  // reports the matched phrase; you map it back to a category client-side.
  const categories = [
    { name: "Churn risk", keywords: ["cancel my contract", "switch provider", "too expensive"] },
    { name: "Compliance", keywords: ["record this call", "speak to a lawyer"] },
  ];
  const categoryOf = (phrase) =>
    categories.find((c) => c.keywords.includes(phrase))?.name ?? "Other";

  class FlagWall extends HTMLElement {
    connectedCallback() {
      const callId = this.getAttribute("call-id");
      const url = `/api/v1/voice/supervisor/calls/${callId}/analytics/stream`;
      this.source = new EventSource(url, { withCredentials: true });
      this.filter = "All";
      this.render(`Waiting for the first settled utterance…`, "muted");

      this.source.addEventListener("status", (e) => {
        const { status, terminal } = JSON.parse(e.data);
        if (terminal) this.render(`Call already ${status} — use post-call quality evaluation.`, "muted");
      });

      this.source.addEventListener("analytics", (e) => {
        const event = JSON.parse(e.data);
        if (event.kind === "keyword") {
          const category = categoryOf(event.phrase);
          if (this.filter !== "All" && category !== this.filter) return;
          this.render(`${category} · "${event.phrase}" ×${event.occurrences} (${event.speaker}): ${event.snippet}`, "flag");
        }
        if (event.kind === "metrics") {
          const m = event.metrics;
          this.render(
            `talk/listen ${m.talk_listen_ratio ?? "–"} · dead air ${Math.round(m.silence_total_ms / 1000)}s over ${m.silence_events} gaps · interruptions ${m.interruptions.total}`,
            "metric",
          );
        }
        if (event.kind === "emotion" && event.stress_score >= 0.7) {
          this.render(`caller stress ${event.label} (${event.stress_score.toFixed(2)})`, "alert");
        }
        if (event.kind === "clone-risk" && event.risk_level === "high") {
          this.render(`synthetic-voice risk ${event.risk_score.toFixed(2)} — ${event.recommended_action}`, "alert");
        }
      });

      // Sessions close themselves after 30 minutes ("max-duration").
      this.source.addEventListener("end", () => this.source.close());
    }
    disconnectedCallback() {
      this.source?.close();
    }
    render(text, tone) {
      const chip = document.createElement("div");
      chip.className = `chip ${tone}`;
      chip.textContent = text;
      this.prepend(chip);
    }
  }
  customElements.define("flag-wall", FlagWall);
</script>
```

To filter by keyword category, add a `<select>` over `categories` and set `this.filter` on change; the handler above already drops keyword chips outside the selected category. Metrics, stress, and clone-risk chips are independent of the category filter.

## Troubleshooting

**The stream opens but only `connected` and `status` arrive.** The call is live but no utterance has settled yet, or transcription is not producing finals for this call. Check that live transcription works for the call first (see [Live call transcription](/voice/live-call-transcription)); the analytics stream derives everything from the same transcript frames. A gap longer than 15 seconds without even a `: ping` means the connection died; reconnect.

**No `keyword` events although agents say the watch phrases.** The watchlist was empty when the stream opened (no keywords in your quality categories), or the phrase spelling differs — matching is case-insensitive but exact on wording after normalisation. Re-check the category keywords and reopen the stream.

**`403` for a supervisor.** Either the caller's role is not supervisor/owner/admin, or the call went through a queue the supervisor is not assigned to. An owner or admin assigns supervisors to queues in team management; the assignment applies to streams opened afterwards.

**`404` for a call that exists.** The call belongs to a different workspace; the stream is tenant-scoped and never confirms calls outside it.

**How many supervisors can watch one call?** The API does not cap concurrent supervisors per call; each open stream is one connection doing its own live computation. For a handful of supervisors per call this is nothing to plan for. If you intend a wallboard where dozens of browsers watch the same calls, fan out through one backend consumer per call instead of one upstream connection per browser.

**Scope disputes.** A supervisor who expects a queue and gets `403` needs to be assigned to that queue by an owner or admin in team management; the assignment applies to streams opened afterwards. Direct-dial calls are visible to every supervisor in the workspace by design; queue routing is what makes a call team-scoped.

## See also

* [Voice supervisor plane model](/concepts/voice-supervisor-plane-model) — how supervisor roles, queues, and live controls fit together.
* [Live call transcription](/voice/live-call-transcription) and [Transcription operations](/voice/transcription-operations) — the transcript surface this stream derives from, including its redaction behaviour.
* [Supervisor live monitoring for voice](/guides/supervisor-live-monitoring-voice) — listen, whisper, and barge on the same call.
* [Supervisor coaching](/guides/supervisor-coaching) — turning a live flag into a coaching moment.
* [Quality evaluation lifecycle](/concepts/quality-evaluation-lifecycle) — the post-call batch pipeline this stream complements.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.