Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions fern/apis/signalwire-rest/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38695,8 +38695,8 @@ components:
coach:
type: string
description: |-
Coach accepts a call SID of a call that is currently connected to an in-progress conference.
Specifying a call SID that does not exist or is no longer connected will result in a failure.
The call ID (a UUID) of a participant already in the conference to coach. This call becomes the coach: only the coached participant hears it, and its audio stays out of the main conference mix.
If the call ID is not a current participant, the join fails. A muted coach is heard by nobody.
examples:
- b3877ee3-6f3c-4985-8066-6d24e3f65e12
status_callback_event:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ puts response.to_s
</ParamField>

<ParamField path="coach" type="string" toc={true}>
Coach accepts a call [SID](/docs/platform/what-is-a-sid) of a call that is currently connected to an in-progress conference. Specifying a call SID that does not exist or is no longer connected to the conference will result in the call failing to the action URL and throwing a 13240 error.
The call [SID](/docs/platform/what-is-a-sid) of a participant already in the conference to coach. This call becomes the coach: only the coached participant hears it, and its audio stays out of the main conference mix. If the SID is not a current participant, the join fails and `<Dial>` completes with a `DialCallStatus` of `busy`. A muted coach is heard by nobody. See [Supervisor call monitoring](/docs/platform/voice/supervisor-call-monitoring) for listen-in, whisper, and barge workflows.
</ParamField>

<ParamField path="endConferenceOnExit" type="boolean" default="false" toc={true}>
Expand Down Expand Up @@ -539,10 +539,12 @@ end
puts response.to_s
```
</CodeBlocks>
Participants who enter a conference call muted can hear the other participants in the call who are unmuted. However, the unmuted participants cannot hear the muted callers. Muting and unmuting can be enabled and disabled in real-time via a REST API.
Participants who enter a conference call muted can hear the other participants in the call who are unmuted. However, the unmuted participants cannot hear the muted callers. Muting and unmuting can be enabled and disabled in real-time via a REST API. For a supervisor workflow that moves between silent monitoring, coaching, and speaking, see [Supervisor call monitoring](/docs/platform/voice/supervisor-call-monitoring).

### Coaching a conference call

The `coach` attribute makes the joining call a coach for the participant whose call SID it names. The coach hears the whole conference, and only the coached participant hears the coach. Everyone else, including a customer on the line, hears nothing from the coach. The coached participant must already be in the conference, and a coach who is also muted is heard by nobody.

<CodeBlocks>
```xml
<?xml version="1.0" encoding="UTF-8"?>
Expand Down Expand Up @@ -577,7 +579,7 @@ class Example
var response = new VoiceResponse();
var dial = new Dial();
dial.Conference("Example-Room",
coach: 'AgentCallSid');
coach: "AgentCallSid");

response.Append(dial);
Console.WriteLine(response.ToString());;
Expand All @@ -601,7 +603,7 @@ require 'signalwire/sdk'

response = Signalwire::Sdk::VoiceResponse.new do |response|
response.dial do |dial|
dial.conference('Example-Room', coach: '')
dial.conference('Example-Room', coach: 'AgentCallSid')
end
end

Expand Down
3 changes: 3 additions & 0 deletions fern/products/platform/pages/calling/voice/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ SignalWire's advanced APIs and elastic cloud infrastructure make it a breeze to
<Card title="Make and receive phone calls with SWML" icon="regular comments" href="/docs/swml/guides/make-and-receive-calls">
Guide that focuses on how to make and receive phone calls via a SWML script.
</Card>
<Card title="Supervisor call monitoring" icon="regular headset" href="/docs/platform/voice/supervisor-call-monitoring">
Listen in on, whisper to, or barge an agent's call from a supervisor console.
</Card>
</CardGroup>

{/* Core links */}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
---
title: Supervisor call monitoring
slug: /voice/supervisor-call-monitoring
description: Listen in on a live call silently, whisper to an agent without the customer hearing, or barge in, using SignalWire conferences from SWML, cXML, REST, or the Server SDKs.
max-toc-depth: 3
---

[join-conference]: /docs/swml/reference/calling/join-conference
[conference-noun]: /docs/compatibility-api/cxml/reference/voice/conference
[update-participant]: /docs/compatibility-api/rest/conference-participants/update-participant
[list-participants]: /docs/compatibility-api/rest/conference-participants/list-all-participants
[call-commands]: /docs/apis/rest/calls/call-commands
[create-a-call]: /docs/compatibility-api/rest/calls/create-a-call
[sdk-py-update]: /docs/server-sdks/reference/python/rest/compat/conferences/update-participant
[sdk-ts-update]: /docs/server-sdks/reference/typescript/rest/compat/conferences/update-participant
[sdk-py-dial]: /docs/server-sdks/reference/python/rest/calling/dial
[sdk-ts-dial]: /docs/server-sdks/reference/typescript/rest/calling/dial
[sid]: /docs/platform/what-is-a-sid
[call-whisper]: /docs/swml/guides/call-whisper
[ai-params]: /docs/swml/reference/calling/ai/params
[ai-sidecar]: /docs/swml/reference/calling/ai-sidecar
[stream]: /docs/swml/reference/calling/stream
[tap]: /docs/swml/reference/calling/tap

A supervisor on a contact-centre floor needs three things from a live call: to hear it without being heard, to coach the agent without the customer hearing, and to step into the conversation outright. SignalWire supports all three on any call that runs inside a conference, and a supervisor console can move between them mid-call with one REST request per switch.

Monitoring works on conference participants only. The agent's call must already be in a SignalWire conference before a supervisor can hear it, so route agent calls through [`join_conference`][join-conference] in SWML or [`<Conference>`][conference-noun] in cXML rather than a plain two-party connect. There is no way to listen to, whisper into, or barge a call that is bridged directly to another call.

## The three modes

Two participant settings drive every mode: `muted` and `coach`. There is no separate listen-in or barge parameter to look for.

| Mode | Supervisor hears | Supervisor is heard by | Settings on the supervisor's call |
|---|---|---|---|
| Listen in | Everyone | Nobody | `muted` on |
| Whisper | Everyone | The agent only | `muted` off, `coach` set to the agent's call ID |
| Barge | Everyone | Everyone | `muted` off, no `coach` |

The `coach` setting names the participant being coached. The call that carries it becomes the coach: its audio goes to the coached participant alone and stays out of the main conference mix, so the customer never hears it. A muted coach is not heard by anyone, so whisper requires the supervisor to be unmuted.

## Before you start

You need three values, all of them from the agent's side of the call:

- **The conference name** the agent's call joined.
- **The agent's call ID.** Read it from the conference `join` status callback, or from [List all active Participants][list-participants]. cXML and the Compatibility REST API call this value the call SID; SWML and the Calling API call it the call ID. Each is the [UUID that identifies the call][sid].
- **The conference SID**, from the same status callback or from the participants list. The REST requests below address the supervisor by conference SID and call ID.

REST requests use a project API token with the Voice scope.

There is no REST request that adds a participant to a conference. To bring a supervisor in, create a call whose instructions join the conference, with [Send call commands][call-commands] on the Calling API for SWML or [Create a call][create-a-call] on the Compatibility API for cXML, or let the supervisor dial a number that runs those instructions.

## Listen in

Join the supervisor's call to the agent's conference with `muted` on. The supervisor hears every unmuted participant and is heard by nobody.

```yaml title="SWML"
version: 1.0.0
sections:
main:
- join_conference:
name: support-floor
muted: true
```

Silent monitoring leaves no marker of its own. A muted supervisor looks like any other muted participant in the participant list and in status callbacks, so if you need a record of who listened to which call, write it in your application from the `join` and `leave` events.

## Whisper

Join unmuted and set `coach` to the agent's call ID. The supervisor hears the whole conference, and only the agent hears the supervisor.

```yaml title="SWML"
version: 1.0.0
sections:
main:
- join_conference:
name: support-floor
coach: b3877ee3-6f3c-4985-8066-6d24e3f65e12
```

The agent must be in the conference when the supervisor joins. If the call ID in `coach` is not a current participant, the join fails: in cXML, `<Dial>` completes with a `DialCallStatus` of `busy` and control passes to its action URL; in SWML, `join_conference_result` reports the failure and the script continues.

## Barge

Join unmuted with no `coach`, and the supervisor is an ordinary participant that everyone hears. In practice a supervisor rarely joins this way. They start by listening or whispering and barge by changing state, as the next section shows.

## Switch modes during the call

Markup sets a participant's state at join time. Changing it afterward is a cross-call operation, so it belongs to the REST API rather than to the supervisor's own SWML or cXML: [Update a Participant][update-participant] on the Compatibility REST API changes `Muted`, `Coaching`, and `CallSidToCoach` for any participant, whether the conference was joined from SWML or cXML. A supervisor console with three buttons makes one request per press.

| From | To | Request body |
|---|---|---|
| Listen in | Whisper | `Muted=false` `Coaching=true` `CallSidToCoach=<agent call ID>` |
| Listen in | Barge | `Muted=false` |
| Whisper | Barge | `Coaching=false` |
| Whisper | Listen in | `Muted=true` `Coaching=false` |
| Barge | Whisper | `Coaching=true` `CallSidToCoach=<agent call ID>` |
| Barge | Listen in | `Muted=true` |

The request is form-encoded, addressed by the conference SID and the supervisor's call ID:

```bash title="Listen in to whisper"
curl -X POST "https://example.signalwire.com/api/laml/2010-04-01/Accounts/$PROJECT_ID/Conferences/$CONFERENCE_SID/Participants/$SUPERVISOR_CALL_ID" \
-u "$PROJECT_ID:$API_TOKEN" \
--data-urlencode "Muted=false" \
--data-urlencode "Coaching=true" \
--data-urlencode "CallSidToCoach=$AGENT_CALL_ID"
```

The only reader this split inconveniences is a supervisor dialing in from a handset with no application behind them. Give that supervisor a console, or a separate number whose instructions join in each mode.

### End to end with the Server SDKs

The Server SDKs cover both halves: [`dial`][sdk-py-dial] on the Calling API places the supervisor's call with inline SWML, and [`update_participant`][sdk-py-update] on the Compatibility API changes its state. The TypeScript equivalents are [`dial`][sdk-ts-dial] and [`updateParticipant`][sdk-ts-update]. The agent's call ID and the conference SID come from the conference's `join` status callback.

<CodeBlocks>
```python {14-22,26-32,35}
from signalwire.rest import RestClient

client = RestClient(
project="your-project-id",
token="your-api-token",
host="your-space.signalwire.com",
)

# Both values arrive on the conference's `join` status callback for the agent's call.
conference_sid = "e2a5f5c3-0b6d-4d7f-9c2a-1f3b4c5d6e7f"
agent_call_id = "b3877ee3-6f3c-4985-8066-6d24e3f65e12"

# 1. Bring the supervisor in silently.
supervisor = client.calling.dial(
from_="+15551234567",
to="+15559876543",
swml={
"version": "1.0.0",
"sections": {"main": [{"join_conference": {"name": "support-floor", "muted": True}}]},
},
)
supervisor_call_id = supervisor["id"]

# 2. Whisper: unmute and coach the agent in one request.
client.compat.conferences.update_participant(
conference_sid,
supervisor_call_id,
Muted=False,
Coaching=True,
CallSidToCoach=agent_call_id,
)

# 3. Barge: stop coaching, and everyone hears the supervisor.
client.compat.conferences.update_participant(conference_sid, supervisor_call_id, Coaching=False)
```

```typescript {14-22,26-30,33}
import { RestClient } from "@signalwire/sdk";

const client = new RestClient({
project: "your-project-id",
token: "your-api-token",
host: "your-space.signalwire.com",
});

// Both values arrive on the conference's `join` status callback for the agent's call.
const conferenceSid = "e2a5f5c3-0b6d-4d7f-9c2a-1f3b4c5d6e7f";
const agentCallId = "b3877ee3-6f3c-4985-8066-6d24e3f65e12";

// 1. Bring the supervisor in silently.
const supervisor = await client.calling.dial({
from: "+15551234567",
to: "+15559876543",
swml: {
version: "1.0.0",
sections: { main: [{ join_conference: { name: "support-floor", muted: true } }] },
},
});
const supervisorCallId = supervisor.id;

// 2. Whisper: unmute and coach the agent in one request.
await client.compat.conferences.updateParticipant(conferenceSid, supervisorCallId, {
Muted: false,
Coaching: true,
CallSidToCoach: agentCallId,
});

// 3. Barge: stop coaching, and everyone hears the supervisor.
await client.compat.conferences.updateParticipant(conferenceSid, supervisorCallId, { Coaching: false });
```
</CodeBlocks>

To confirm the switch took effect, retrieve the supervisor's participant record. `coaching` is `true` and `call_sid_to_coach` holds the agent's call ID while whispering, and `muted` is `true` while listening in. If the update returns error 16002, `Coaching` was `true` without a `CallSidToCoach`, or the call ID in `CallSidToCoach` is not in that conference.

## Keep the console in sync

Subscribe to participant events so the console's buttons reflect state set anywhere, including a mute from another tool. In SWML, set `status_callback` and `status_callback_event` on the `join_conference` that creates the conference; in cXML, set `statusCallback` and `statusCallbackEvent` on `<Conference>`. The `join`, `leave`, `mute`, `hold`, and `speaker` events carry each participant's mute state and coaching state: `muted` and `coaching` in SWML callbacks, `Muted`, `Coaching`, and `CallSidToCoach` in cXML callbacks.

## Limits

- **Conference only.** A directly bridged two-party call cannot be monitored. Put agent calls in a conference from the start if supervisors may need to join.
- **The agent must be present first.** A `coach` that names a call outside the conference fails the join, and a REST update naming one returns error 16002.
- **A participant cannot coach itself.**
- **Muted wins.** A muted coach is heard by nobody. Whisper needs `Muted=false` alongside `Coaching=true`.
- **The coach leaves with the agent.** When the coached participant leaves the conference, every participant coaching them is removed from it too, and each of those calls continues with whatever follows the join in its own instructions. Switch a supervisor to listen in or barge before the agent hangs up if they need to stay.
- **Several supervisors can coach one agent** at the same time. Each coach whispers to one participant.
- **Same project.** A supervisor can only join conferences in the project that created them.

## Recording and consent

<Warning title="Not legal advice">
Listening to, coaching on, or joining a call counts as monitoring or recording under laws that vary by country, by state, and by whether the call is inbound or outbound. Many require notice to one or both parties. SignalWire plays no notice and performs no consent check for you. Your application is responsible for any announcement, consent capture, and record-keeping the law requires. Consult qualified counsel before deploying supervisor monitoring.
</Warning>

Coaching state is visible: the participant record and the participant status callbacks report `coaching` and the coached call ID. Silent monitoring is not distinguished from any other muted participant, so keep your own log if you need one.

## Not the same as

Several features share this vocabulary and do something else:

- **[Call whisper][call-whisper]** in SWML plays an announcement to the person answering before the caller is connected. It is call screening, not coaching.
- **AI barge** in [`ai.params`][ai-params] controls whether a caller can interrupt the AI agent's speech. It has nothing to do with a human joining a call.
- **[`ai_sidecar`][ai-sidecar]** observes a call and emits text for a UI. It injects no audio and is not a participant.
- **[`stream`][stream] and [`tap`][tap]** fork call audio to a WebSocket or RTP endpoint. The receiver gets a media stream, not a call leg, and cannot speak into the call.

## Next steps

<CardGroup cols={2}>
<Card title="join_conference" href="/docs/swml/reference/calling/join-conference" icon="regular users">
Every SWML conference parameter, including `muted`, `coach`, and the status callback events.
</Card>
<Card title="cXML Conference noun" href="/docs/compatibility-api/cxml/reference/voice/conference" icon="regular code">
The `<Conference>` attributes and the status callback fields.
</Card>
<Card title="Update a Participant" href="/docs/compatibility-api/rest/conference-participants/update-participant" icon="regular rectangle-api">
The REST request that switches a participant between modes.
</Card>
<Card title="Server SDK update_participant" href="/docs/server-sdks/reference/python/rest/compat/conferences/update-participant" icon="regular server">
The same request from Python, with a TypeScript twin.
</Card>
</CardGroup>
3 changes: 2 additions & 1 deletion fern/products/platform/pages/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,10 @@ This is common for:
**If you need straightforward call handling** (IVRs, call forwarding, playing messages), use **[SWML](/docs/swml)**.
Your server responds to webhooks with JSON/YAML instructions. It's stateless and works with any programming language.

**If you need realtime control** (live call monitoring, mid-call transfers, complex orchestration), use the **[RELAY client](/docs/server-sdks/guides/relay-client)**.
**If you need realtime control** (reacting to call events as they happen, mid-call transfers, complex orchestration), use the **[RELAY client](/docs/server-sdks/guides/relay-client)**.
It maintains a persistent WebSocket connection for instant, bi-directional communication.
Best for applications that need to react to events as they happen.
For a supervisor who listens in on, whispers to, or barges an agent's call, see [Supervisor call monitoring](/docs/platform/voice/supervisor-call-monitoring).

**If you're building AI voice agents**, use the **[Server SDKs](/docs/server-sdks)**.
They're designed for creating conversational AI that handles phone calls, in the language of your choice:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ used internally. Passing any non-default parameter triggers the full object form
</ParamField>

<ParamField path="coach" type="Optional[str]" default="None" toc={true}>
SWML Call ID or CXML CallSid of a participant who can coach (whisper to)
this participant without other participants hearing.
Call ID of the participant to coach. The call that sets this becomes the
coach: only the coached participant hears it, and its audio stays out of the
main conference mix. See [Supervisor call monitoring](/docs/platform/voice/supervisor-call-monitoring).
</ParamField>

<ParamField path="status_callback_event" type="Optional[str]" default="None" toc={true}>
Expand Down
Loading
Loading