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

# Real-time monitoring

> Watch live conversations over a WebSocket and send control commands: end a call, transfer it, add context, or take over a chat

Your systems can connect to a conversation while it happens. A WebSocket streams
what the customer and the agent say, the tools the agent runs, and when the
conversation ends. The same connection takes control commands. Use it for quality
assurance, live dashboards, supervising calls, or stepping in when a conversation
needs a person.

Monitoring works on phone and website calls and on website, mobile SDK and API
chats. WhatsApp, Messenger, Instagram, Slack, email and SMS conversations cannot
be monitored yet. Monitoring is available on every plan.

> [!NOTE]
>
> Monitoring shows customers' words to whoever holds the key. Tell your customers
> where the law requires it, the same way you would for call recording or quality
> review.

## Turn it on

1. Open the agent, then **Settings → Advanced**.
2. In the **Monitoring** card, turn on **Monitoring**.
3. Under **Events**, choose what a monitor receives.
4. Choose **Save**.

![Monitoring card in Advanced settings with the Monitoring switch on and the selected events shown as tags](https://mintcdn.com/centillion/3iO-lt3tVIT5wfUB/images/api-reference/realtime-monitoring-01-settings.webp?fit=max\&auto=format\&n=3iO-lt3tVIT5wfUB\&q=85\&s=65e98b963b196aa1f66155988fd5869f)

The Monitoring card with its selected events.

The setting belongs to the agent, so every branch shares it, and you change it on
Main. It needs no publish. It applies to calls that start after you save and to
chats from their next message, and turning it off closes open connections within
15 seconds.

## Connect

```text theme={null}
wss://app.cntillion.com/api/v2/agents/{agent_id}/conversations/{conversation_id}/monitor
```

To connect you need three things:

| Requirement | What it means |
| - | - |
| API key permission | A key with **Monitor** or **Full access** from **Workspace settings → API keys**. A **Read only** key is refused. **Monitor** can read like **Read only** and watch conversations, and cannot change agents or data. |
| Workspace access | The person who created the key must still be an Owner or Member of the workspace. If they leave, or become a Viewer or Support Associate, the key stops monitoring until you replace it. |
| Header | `Authorization: Bearer cl_api_...` or `X-API-Key: cl_api_...` |

Browsers cannot set headers on a WebSocket, so connect from your server. Keep API
keys out of browser code.

```javascript Node.js theme={null}
import WebSocket from "ws";

const socket = new WebSocket(
  `wss://app.cntillion.com/api/v2/agents/${agentId}/conversations/${conversationId}/monitor`,
  { headers: { "X-API-Key": process.env.CENTILLION_API_KEY } }
);

socket.on("message", (data) => {
  const event = JSON.parse(data);
  console.log(event.event_id, event.type);
});
```

```python Python theme={null}
import asyncio
import json
import os

import websockets


async def monitor(agent_id, conversation_id):
    url = (
        "wss://app.cntillion.com/api/v2/agents/"
        f"{agent_id}/conversations/{conversation_id}/monitor"
    )
    headers = {"X-API-Key": os.environ["CENTILLION_API_KEY"]}
    async with websockets.connect(url, additional_headers=headers) as socket:
        async for message in socket:
            event = json.loads(message)
            print(event["event_id"], event["type"])
```

Everything that can refuse a connection answers as ordinary HTTP, before the
socket opens. To check a key and a conversation, send the same URL with `curl`
over `https://`. If nothing stands in the way, it answers `426`.

| Status | Code | Meaning |
| - | - | - |
| 401 | `UNAUTHORIZED` | The key is missing, invalid, expired or revoked. |
| 403 | `FORBIDDEN` | The key is **Read only**. |
| 403 | `WORKSPACE_ACCESS_REQUIRED` | The key's creator is no longer an Owner or Member. |
| 403 | `MONITORING_DISABLED` | Monitoring is off for this agent. |
| 404 | `AGENT_NOT_FOUND`, `CONVERSATION_NOT_FOUND` | Not found in the key's workspace. |
| 409 | `CONVERSATION_NOT_ACTIVE` | The conversation has ended, or started before monitoring was on. |
| 426 | `UPGRADE_REQUIRED` | Not a WebSocket request. |
| 429 | `MONITOR_CONNECTION_LIMIT`, `RATE_LIMIT_TOO_MANY_REQUESTS` | See [Limits](#limits). |

## Find a conversation

A conversation must be in progress before you can connect. List the ones you can
monitor with the [conversations endpoint](https://cntillion.com/docs/api-reference/developer-api.md):

```bash theme={null}
curl 'https://app.cntillion.com/api/v2/agents/YOUR_AGENT_ID/conversations?monitorable=true' \
  -H "Authorization: Bearer $CENTILLION_API_KEY"
```

Each result's `id` is the `conversation_id`. A call counts until it ends. A text
conversation counts until it ends, or for a day after its last message.

## Events

When you connect you receive the conversation's last 100 or so events, then new
ones as they happen. Every event has these fields, plus the body shown below:

```json theme={null}
{
  "type": "user_transcript",
  "event_id": 12,
  "conversation_id": "conversation-id",
  "timestamp": "2026-10-31T12:00:00.000Z",
  "user_transcription_event": { "user_transcript": "Where is my order?" }
}
```

`event_id` counts up within a conversation. It is how you tell events apart after
a reconnect. Events reach you about a second after they happen, and a call's
final words can take a few seconds to arrive on a Realtime voice.

You choose among these on the agent:

| Event | Body | When |
| - | - | - |
| `conversation_initiation_metadata` | `conversation_initiation_metadata_event`: `channel`, `mode` (`voice` or `text`) | The conversation starts. |
| `user_transcript` | `user_transcription_event`: `user_transcript` | The customer finished a line or sent a message. |
| `agent_response` | `agent_response_event`: `agent_response`, `response_id` | The agent's message is complete. |
| `agent_response_correction` | `agent_response_correction_event`: `original_agent_response`, `corrected_agent_response` | The caller cut the agent off. The corrected text is what they heard. Custom voice only. |
| `agent_response_complete` | `agent_response_complete_event` | The agent finished its turn, tools included. |
| `client_tool_call` | `client_tool_call`: `tool_name`, `tool_call_id`, `parameters` | The agent asked a website or app to run a function. |
| `agent_tool_response` | `agent_tool_response`: `tool_name`, `tool_call_id`, `tool_type`, `is_error` | The agent ran a tool. `tool_type` is `system`, `workspace` or `client`. |
| `agent_tool_response_full_payload` | `agent_tool_response_full_payload`: the same, plus `full_tool_result` and `truncated` | As above, with the tool's result, cut at 64 KB. |
| `agent_chat_response_part` | `text_response_part`: `type` (`start`, `delta` or `stop`), `text`, `response_id` | The reply as it is written. Text conversations and Custom voice only, and not while a guardrail has to approve the whole reply. |
| `guardrail_triggered` | None | A guardrail ended the call. Calls only. |

New monitors start with everything except `agent_tool_response_full_payload` and
`agent_chat_response_part`. A tool's result can hold anything the tool returned,
so select it only when your client can be trusted with it.

A workflow can only mask a sensitive value once it has read and stored it, so a
conversation with sensitive variables or a keypad step works in two ways:

* On a step that can take a sensitive value, you get the events but not the words:
  `user_transcript`, `agent_response`, corrections and full tool results read
  `•••`, and tool arguments are empty. This covers every Collect step, a keypad
  step, and a step whose way out stores a sensitive value, for as long as the
  conversation is on that step. The conversation's stored transcript keeps its
  usual text.
* Every other step is sent as usual, with stored values masked wherever they
  appear. In a chat, the customer's message reaches you with the agent's reply,
  and a turn that stores a value shows `•••` for that turn.

`agent_chat_response_part` is not sent for a conversation that can hold a
sensitive value, because a value could fall across two parts.

Masking finds a value in its digits or its exact text. If a customer gives a value
on a step that does not take it, or says it in another form such as a date in
words, it can reach you unmasked. Replays mask what the workflow has stored by
then.

Audio, voice activity scores, turn probability and pings cannot be monitored.

These always arrive, whatever you select:

| Event | Body | When |
| - | - | - |
| `command_result` | `command_result_event`: `command_id`, `command_type`, `status`, `code`, `message` | A command finished. See [Results](#results). |
| `human_message` | `human_message_event`: `text` | A monitor sent the customer a message. |
| `conversation_ended` | `conversation_ended_event`: `reason` (`call_ended`, `ended_by_monitor` or `workflow_ended`) | The conversation is over. The socket closes two seconds later. |

## Control commands

Send one JSON message per command. Add an optional `command_id` of your own, and
it comes back on the result.

| Command | Calls | Chats |
| - | - | - |
| `end_call` | Yes | Yes |
| `transfer_to_number` | Phone calls | No |
| `contextual_update` | Yes | Yes |
| `enable_human_takeover` | No | Yes, with Helpdesk on |
| `send_human_message` | No | Yes, during a takeover |
| `disable_human_takeover` | No | Yes, during a takeover |

### End the conversation

```json theme={null}
{ "command_type": "end_call" }
```

A call hangs up at once. A chat closes, and the customer's next message is told
the conversation has ended. While another ticket is open on a chat during a
takeover, the command is refused until that one closes.

### Transfer to a phone number

```json theme={null}
{
  "command_type": "transfer_to_number",
  "parameters": { "phone_number": "+15551234567" }
}
```

The agent stops talking and the caller moves to the number. Turn on the **Transfer
to number** tool for the agent first. A number bought through Centillion can
transfer only within its own country, and never to premium-rate numbers. If the
carrier refuses the transfer, the agent stays on the line and the result says
`transfer_failed`.

### Add context

```json theme={null}
{
  "command_type": "contextual_update",
  "parameters": { "contextual_update": "This customer is on the Premium plan." }
}
```

The agent uses it in its replies from then on. It does not interrupt or trigger a
reply, the customer never sees it, and it stays out of the transcript. Up to 4,000
characters. A chat keeps the latest 20 updates. A call takes 40,000 characters of
updates in all, and fails the rest with `context_full`.

### Take over a chat

```json theme={null}
{ "command_type": "enable_human_takeover" }
```

The AI stops answering and a live chat ticket opens in the
[Helpdesk](https://cntillion.com/docs/helpdesk.md), so your team can see it and join. Turn on Helpdesk for the
agent first. API conversations cannot be taken over, because nothing shows them a
person's reply. To reach a person on a call, use `transfer_to_number`.

While you hold the conversation, the customer's messages keep arriving as
`user_transcript`.

### Send a message as a person

```json theme={null}
{
  "command_type": "send_human_message",
  "parameters": { "text": "Hi, this is Sam. How can I help?" }
}
```

The customer sees it from `Support`. Up to 4,000 characters.

### Give the chat back to the AI

```json theme={null}
{ "command_type": "disable_human_takeover" }
```

The ticket closes and the AI answers the next message, whatever your
[Helpdesk close setting](https://cntillion.com/docs/helpdesk.md) says. If another ticket is open on the same
conversation, the command is refused until that one closes.

### Results

A command that ran is reported to every monitor of the conversation:

```json theme={null}
{
  "type": "command_result",
  "event_id": 31,
  "conversation_id": "conversation-id",
  "timestamp": "2026-10-31T12:00:00.000Z",
  "command_result_event": {
    "command_id": "my-id",
    "command_type": "transfer_to_number",
    "status": "failed",
    "code": "transfer_failed"
  }
}
```

`status` is `applied` or `failed`. A failed command has a `code`:
`transfer_failed`, `room_not_closed`, `agent_unavailable`, `call_ended`,
`context_full`, `not_a_phone_call`, `timed_out` or `internal_error`.

A command that never ran is answered only to the monitor that sent it, with
`status: "refused"` and no `event_id`:

| Code | Meaning |
| - | - |
| `invalid_command` | The message is not a command this endpoint knows. |
| `rate_limited` | Too many commands. |
| `chat_only` | Human takeover works on chats. |
| `not_a_phone_call` | Only a phone call can be transferred. |
| `transfer_not_configured` | The agent's **Transfer to number** tool is off. |
| `transfer_refused` | This line cannot call that number. |
| `helpdesk_required` | Helpdesk is off for this agent. |
| `takeover_not_supported` | This channel cannot show a person's reply. |
| `takeover_not_enabled` | There is no takeover to act on. |
| `other_open_tickets` | Another open ticket keeps the conversation with your team. |

## Staying connected

A connection lasts about 13 minutes, then closes with code `1012`. Reconnect with
the last `event_id` you saw, and you receive only what came after it:

```text theme={null}
wss://app.cntillion.com/api/v2/agents/{agent_id}/conversations/{conversation_id}/monitor?after=42
```

| Close code | Meaning | What to do |
| - | - | - |
| 1000 | The conversation ended. | Stop. |
| 1011 | Something failed on our side. | Reconnect after a short wait. |
| 1012 | The connection reached its time limit. | Reconnect with `after`. |
| 1013 | Your client read too slowly. | Reconnect with `after`. |
| 4401 | The key was revoked or expired. | Use another key. |
| 4403 | Monitoring was turned off, or the person who created the key can no longer edit agents. | Turn monitoring on for new conversations, or use another key. |

## Limits

* About the last 100 events of a conversation are available. Earlier ones are gone
  for a monitor, though the [stored conversation](https://cntillion.com/docs/api-reference/developer-api.md)
  keeps its messages.
* Each conversation takes 10 connections, and each key 50.
* Each connection takes 20 commands in 10 seconds, and connecting counts toward
  the key's [rate limit](https://cntillion.com/docs/api-reference/developer-api.md).
* A conversation must already be in progress on an agent with monitoring on. You
  cannot connect before it starts or after it ends.
* Events carry text and metadata, never audio.


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