> ## Documentation Index
> Fetch the complete documentation index at: https://docs.retellai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Monitor Call WebSocket

> Stream a live call transcript in real time with the Retell AI monitor call WebSocket: connect with your API key and get transcript, tool call, and node events.

<Note>
  This is the stream behind [Live Monitoring](/features/live-monitoring) in the dashboard. For a call that has already ended, use [Get Call](/api-references/get-call) instead: it returns the final transcript with word timestamps and, when PII scrubbing is on, the scrubbed version.
</Note>

## Overview

The monitor call WebSocket streams a live call's transcript to your server while the call is in progress. Connect with your API key to receive every agent and user turn, tool call, and node transition as JSON events, starting with a snapshot of the conversation so far and ending when the call ends.

Your server opens this connection and Retell sends events on it. Your server doesn't send anything back.

Use it when you need the conversation while it's still happening:

* **Show live transcripts in your own tools.** Build a supervisor console, embed the transcript in your CRM, or mirror the call in an internal dashboard.
* **React mid-call.** Page a human when the flow reaches an escalation node, alert on a failed tool call, or flag a keyword before the call ends.
* **Audit in real time.** Watch sensitive conversations as they happen instead of only in review.

For example, a dental clinic streams every appointment call into its own operations console. When the agent reaches the "Escalate to staff" node, the clinic's server posts the last few turns to the front-desk channel, and a receptionist picks up the call from the dashboard with [Take Over](/features/live-monitoring#take-over).

## Endpoint

<Note>WebSocket endpoint: `wss://api.retellai.com/v2/monitor-call/{call_id}`</Note>

### Path parameters

<ParamField path="call_id" type="string" required>
  Id of the call to monitor. The call must belong to your workspace and be in progress (`call_status` is `ongoing`).
</ParamField>

### Headers

<ParamField header="Authorization" type="string" required>
  `Bearer YOUR_RETELL_API_KEY`, the same as for REST requests. A key with restricted permissions needs **Call → Edit**. Keys without restrictions work as is. See [Manage API keys](/accounts/manage-api-keys).
</ParamField>

Connect from a server, not a browser. Browsers can't set headers on a WebSocket handshake, so a browser client would have to expose your API key. To show the live transcript in a web page, relay the events through your own backend.

### Connection limits

* **The call must be `ongoing`.** A registered call that hasn't connected yet, or a call that has ended, is rejected with close code 4004. Connect when you receive the `call_started` [webhook](/features/webhook-overview), or once Get Call reports `ongoing`.
* **Up to 5 connections per call** (as of September 2026). Dashboard viewers count toward the same limit. A sixth connection is rejected with close code 4008 and the reason `max watchers reached`.

## Protocol

Retell sends events to your server over this WebSocket following the protocol below. All messages are text frames whose `data` is a stringified JSON object. Check the `type` field to tell events apart.

The stream carries text only. Transcript items have the text and a start time, not per-word timing, so fetch the call with Get Call after it ends for word timestamps. During an [agentic warm transfer](/build/conversation-flow/call-transfer-node), the transfer agent's conversation with the transfer target isn't streamed; it appears in the Get Call transcript as `transfer_target` utterances after the call. The text is always the raw transcript: [PII scrubbing](/accounts/privacy-disable) runs after a call ends, so no scrubbed version exists mid-call. Treat the stream as sensitive data. To hear live audio, use [Live Listen](/features/live-monitoring#live-listen) in the dashboard.

### Event flow

Once Retell validates your key and the call, it admits the connection and sends one [transcript snapshot event](#transcript-snapshot-event) with everything said so far, so a connection that joins mid-call starts complete.

As the call goes on, each change arrives as a [transcript updated event](#transcript-updated-event): a new user or agent turn, more text on the current turn, a tool call and its result, a node transition, a keypad press, an SMS, or injected context.

When the call ends, Retell sends a [call ended event](#call-ended-event) and closes the connection with code 1000 and the reason `call_ended`.

Every [transcript item](#transcript-item-spec) has a stable `id`. When an id arrives again, the new item replaces the old one: as a speaker keeps talking, their current turn is re-sent with the full text so far. Keep items in a map keyed by `id` and sort by `time_sec` for display, and you have the live transcript. The snapshot can arrive after the first updates, so apply the same replace-by-id logic to both.

If your connection drops while the call is still ongoing, reconnect. The new connection gets a fresh snapshot, and replace-by-id handles any overlap. If the call's server disappears without sending `call_ended` (for example, an infrastructure failure), Retell closes the connection within about a minute with code 1000 and the reason `call_ended`, possibly without a call ended event first.

### Retell -> your server event spec

Retell sends three kinds of events to your server in this WebSocket. To differentiate between them, check the `type` field:

* `transcript_snapshot`: sent once after the connection is admitted, with the whole transcript so far
* `transcript_updated`: sent whenever the transcript changes
* `call_ended`: sent once when the call ends, right before Retell closes the connection

#### Transcript snapshot event

Sent once, shortly after the connection is admitted. Retell requests the snapshot from the call's server after your connection opens, so it can arrive after the first transcript updated events.

<ParamField body="type" type="enum<string>" required>
  Differentiate what this event is.

  Available options: `transcript_snapshot`
</ParamField>

<ParamField body="transcripts" type="object[]" required>
  Every [transcript item](#transcript-item-spec) in the call so far, sorted by `time_sec`. Empty if nothing has been said yet.
</ParamField>

<ParamField body="pre_session_transcripts" type="object[]" required>
  Calls made by the agent's pre-session tools before the conversation started, as `tool_call_invocation` and `tool_call_result` items. Empty when the agent has no pre-session tools.
</ParamField>

#### Transcript updated event

Sent whenever the transcript changes. Each event carries one changed item. The exception is a pre-session update, where `transcripts` is empty and `pre_session_transcripts` carries the full pre-session list.

<ParamField body="type" type="enum<string>" required>
  Differentiate what this event is.

  Available options: `transcript_updated`
</ParamField>

<ParamField body="transcripts" type="object[]" required>
  The [transcript item](#transcript-item-spec) that changed, as a one-element array. An item whose id you have already seen replaces the earlier version. Empty when the event only carries `pre_session_transcripts`.
</ParamField>

<ParamField body="pre_session_transcripts" type="object[]">
  Present only when pre-session tool results arrive after you connect. Contains every pre-session item so far, in the same shape as the snapshot.
</ParamField>

#### Call ended event

The last message before Retell closes the connection with code 1000.

<ParamField body="type" type="enum<string>" required>
  Differentiate what this event is.

  Available options: `call_ended`
</ParamField>

<ParamField body="event_timestamp" type="integer">
  When the call ended, in milliseconds since the Unix epoch.
</ParamField>

<ParamField body="disconnection_reason" type="string">
  Why the call ended, such as `user_hangup` or `agent_hangup`. Same values as `disconnection_reason` in the [Get Call](/api-references/get-call) response.
</ParamField>

### Transcript item spec

Each item in `transcripts` and `pre_session_transcripts` carries the fields below, plus the fields for its `role`. The `id` is the role followed by a per-role counter, such as `agent_2` or `tool_call_invocation_0`.

<ParamField body="id" type="string" required>
  Stable id for this item. A later item with the same id replaces this one.
</ParamField>

<ParamField body="time_sec" type="number" required>
  When the item started, in seconds from the start of the call. For `user` and `agent` items this is the start of the first word, or `0` before any word timing exists. Sort by this field for display and keep arrival order for ties.
</ParamField>

<ParamField body="role" type="enum<string>" required>
  What kind of item this is.

  Available options: `user`, `agent`, `tool_call_invocation`, `tool_call_result`, `node_transition`, `dtmf`, `sms`, `injected`
</ParamField>

#### User and agent items

A spoken turn.

<ParamField body="content" type="string" required>
  The text of the turn so far. It grows while the speaker continues, and each change re-sends the item. When the caller's speech couldn't be recognized, `content` is `(unintelligible audio)`.
</ParamField>

#### Tool call invocation item

The agent called a tool. Match it to its result with `tool_call_id`.

<ParamField body="tool_call_id" type="string" required>
  Unique id of the tool call.
</ParamField>

<ParamField body="name" type="string" required>
  Name of the tool.
</ParamField>

<ParamField body="arguments" type="string" required>
  Arguments passed to the tool, as a stringified JSON object.
</ParamField>

<ParamField body="type" type="enum<string>">
  Kind of tool.

  Available options: `custom`, `code`, `mcp`, `integration_app`, `end_call`, `transfer_call`, `bridge_transfer`, `cancel_transfer`, `agent_swap`, `press_digit`, `send_sms`, `extract_dynamic_variable`, `adjust_voice_speed`, `book_appointment_cal`, `check_availability_cal`
</ParamField>

#### Tool call result item

The tool the agent called returned.

<ParamField body="tool_call_id" type="string" required>
  Id of the invocation this result belongs to.
</ParamField>

<ParamField body="content" type="string" required>
  What the tool returned, often a stringified JSON object.
</ParamField>

<ParamField body="successful" type="boolean">
  Whether the tool call succeeded. Absent when the outcome wasn't recorded, which doesn't mean it failed.
</ParamField>

#### Node transition item

A conversation flow agent moved to another node.

<ParamField body="former_node_id" type="string" required>
  Id of the node the agent left.
</ParamField>

<ParamField body="former_node_name" type="string" required>
  Name of the node the agent left.
</ParamField>

<ParamField body="new_node_id" type="string" required>
  Id of the node the agent entered.
</ParamField>

<ParamField body="new_node_name" type="string" required>
  Name of the node the agent entered.
</ParamField>

<ParamField body="transition_type" type="enum<string>" required>
  How the node was reached: `normal` for a regular edge, `global` for a global node, `global_go_back` when returning from a global node, or `interrupt_go_back` when returning after a user interruption.

  Available options: `normal`, `global`, `global_go_back`, `interrupt_go_back`
</ParamField>

#### DTMF item

The caller pressed a key on their phone keypad.

<ParamField body="digit" type="string" required>
  The key pressed: a single character such as `1`, `*`, or `#`.
</ParamField>

#### SMS item

An SMS the caller sent during the call, for example while the agent was leaving a voicemail. Not part of the spoken conversation.

<ParamField body="content" type="string" required>
  Text of the message.
</ParamField>

<ParamField body="multimedia" type="object[]">
  MMS attachments. Display only.

  <Expandable title="properties">
    <ResponseField name="url" type="string" required>
      Signed URL of the attachment.
    </ResponseField>

    <ResponseField name="summary" type="string">
      Short description of the attachment, when available.
    </ResponseField>
  </Expandable>
</ParamField>

#### Injected item

Context your server added mid-call with [Update Live Call](/api-references/update-live-call). Not spoken by either party.

<ParamField body="content" type="string" required>
  The injected text.
</ParamField>

### Close codes

Retell closes the connection with a standard WebSocket close frame. The reason text says why.

| Code   | Meaning                                                                                                                           |
| ------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `1000` | The call ended. Reason `call_ended`.                                                                                              |
| `4000` | The request was malformed.                                                                                                        |
| `4001` | The API key is missing or invalid.                                                                                                |
| `4003` | The key lacks the **Call → Edit** permission.                                                                                     |
| `4004` | The call doesn't exist in your workspace or isn't `ongoing`. Reason `Call not live` for a call that exists but isn't in progress. |
| `4008` | The call already has 5 connections. Reason `max watchers reached`.                                                                |
| `1011` | Internal error on Retell's side. Reconnect with backoff.                                                                          |

## Sample events

### Retell -> your server sample events

<CodeGroup>
  ```json Transcript Snapshot theme={"dark"}
  {
    "type": "transcript_snapshot",
    "transcripts": [
      {
        "id": "agent_0",
        "role": "agent",
        "content": "Hi, this is Ava from Lakeside Dental. Am I speaking with Jordan?",
        "time_sec": 0.9
      },
      {
        "id": "user_0",
        "role": "user",
        "content": "Yes, that's me.",
        "time_sec": 5.4
      }
    ],
    "pre_session_transcripts": []
  }
  ```

  ```json Transcript Updated theme={"dark"}
  {
    "type": "transcript_updated",
    "transcripts": [
      {
        "id": "agent_1",
        "role": "agent",
        "content": "Sure, I can move your cleaning. What day works best?",
        "time_sec": 8.2
      }
    ]
  }
  ```

  ```json Call Ended theme={"dark"}
  {
    "type": "call_ended",
    "event_timestamp": 1757000000000,
    "disconnection_reason": "user_hangup"
  }
  ```
</CodeGroup>

### Transcript item sample events

Each of these appears inside the `transcripts` array of a snapshot or update. Pre-session tool calls use the same shapes inside `pre_session_transcripts`.

<CodeGroup>
  ```json Spoken Turn theme={"dark"}
  // Sent while the caller is still talking
  {
    "id": "user_1",
    "role": "user",
    "content": "Can we do Thursday",
    "time_sec": 12.7
  }

  // Re-sent with the same id and the full text once the caller finishes
  {
    "id": "user_1",
    "role": "user",
    "content": "Can we do Thursday afternoon?",
    "time_sec": 12.7
  }
  ```

  ```json Tool Call Invocation theme={"dark"}
  {
    "id": "tool_call_invocation_0",
    "role": "tool_call_invocation",
    "time_sec": 15.3,
    "tool_call_id": "call_8f2e1c",
    "name": "reschedule_appointment",
    "arguments": "{\"patient_id\": \"p_4821\", \"new_time\": \"2026-09-10T14:00:00-07:00\"}",
    "type": "custom"
  }
  ```

  ```json Tool Call Result theme={"dark"}
  {
    "id": "tool_call_result_0",
    "role": "tool_call_result",
    "time_sec": 16.1,
    "tool_call_id": "call_8f2e1c",
    "content": "{\"status\": \"confirmed\", \"time\": \"2026-09-10T14:00:00-07:00\"}",
    "successful": true
  }
  ```

  ```json Node Transition theme={"dark"}
  {
    "id": "node_transition_2",
    "role": "node_transition",
    "time_sec": 17.0,
    "former_node_id": "node_reschedule",
    "former_node_name": "Reschedule appointment",
    "new_node_id": "node_confirm",
    "new_node_name": "Confirm details",
    "transition_type": "normal"
  }
  ```

  ```json DTMF theme={"dark"}
  {
    "id": "dtmf_0",
    "role": "dtmf",
    "time_sec": 21.4,
    "digit": "1"
  }
  ```

  ```json SMS theme={"dark"}
  {
    "id": "sms_0",
    "role": "sms",
    "time_sec": 24.8,
    "content": "Running late, call me back in 5"
  }
  ```

  ```json Injected theme={"dark"}
  {
    "id": "injected_0",
    "role": "injected",
    "time_sec": 30.2,
    "content": "The patient's insurance was verified this morning."
  }
  ```
</CodeGroup>

## FAQ

<AccordionGroup>
  <Accordion title="Can I get the call audio over this WebSocket?">
    No. The stream is transcript only. Live audio is available in the dashboard through [Live Listen](/features/live-monitoring#live-listen), and the recording is available from Get Call after the call ends.
  </Accordion>

  <Accordion title="Why do I get close code 4004 right after creating a call?">
    The call hasn't connected yet. Monitoring requires a `call_status` of `ongoing`, and a call stays `registered` until the callee answers or the web client joins. Wait for the `call_started` webhook, or poll Get Call, then connect.
  </Accordion>

  <Accordion title="Why does the same transcript item arrive more than once?">
    That's how updates work. A user or agent turn is re-sent with more text as the speaker continues, and the snapshot can overlap with updates that arrived first. Replace items by `id`.
  </Accordion>

  <Accordion title="Do I need to send anything on the socket?">
    No. Retell sends every event and closes the connection when the call ends. There is no config, ping, or acknowledgement to send back, unlike the [LLM WebSocket](/api-references/llm-websocket).
  </Accordion>

  <Accordion title="Does this work for web calls and chats?">
    Phone calls and web calls, yes. Chats aren't calls and can't be monitored with this WebSocket.
  </Accordion>

  <Accordion title="Can I connect from a browser?">
    Only through your own backend. Browsers can't set the `Authorization` header on a WebSocket handshake, and your API key must stay server side. Relay the events from your server to the browser.
  </Accordion>
</AccordionGroup>
