Skip to content
Draft
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
5 changes: 2 additions & 3 deletions fern/apis/signalwire-rest/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42396,9 +42396,8 @@ components:
coach:
type: string
description: |-
Joins the conference in coaching mode for the participant identified by the given call SID.
The coach hears the whole conference, but is heard only by the coached participant; the other participants cannot hear the coach.
The call SID must belong to a call that is currently connected to the in-progress conference. Specifying a call SID that does not exist or is no longer connected will result in a failure.
Joins the conference as a coach for the participant identified by the given call ID (a UUID). The coach hears the whole conference but is heard only by the coached participant; the other participants cannot hear the coach.
The call ID must belong to a participant currently in the in-progress conference, or 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 @@ -84,6 +84,9 @@ SignalWire's advanced APIs and elastic cloud infrastructure make it a breeze to
<Card title="Record calls" icon="regular microphone" href="/docs/platform/voice/record-calls">
Capture a whole call, a single message, or a conference, then manage the files.
</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,259 @@
---
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
Comment on lines +2 to +5

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title and slug, nits. mdx-capability-guides wants a verb-first task title, e.g. "Monitor and coach live calls" or "Listen in, whisper, and barge on calls". The sibling guides use a bare slug (voice/record-calls, voice/call-queues), so drop the leading slash on line 3 to match.

---

[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-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-center 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
```
Comment on lines +55 to +62

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listen-in join beeps the whole conference. beep defaults to "true", and conference_node_add_member (new_conference_controller.c) turns that into beep_on_join/beep_on_leave. On add-member and on leave the controller calls conference_node_play(node, …ENTER_SOUND/EXIT_SOUND), which plays to every participant, the customer included. So a supervisor following this sample is announced by a tone on the way in and out, which defeats silent monitoring, and line 64's "leaves no marker of its own" isn't true as written.

Every supervisor join on the page needs beep: "false", and it has to be the string. The SWML validator (swml_schema.c, SWML_CHECK_OPTIONAL_STRING_MATCHES_PARAM(beep, …, "true,false,onEnter,onExit")) and the Relay check (relay_apis.c call_join_conference) both reject a YAML boolean false.

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


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
```
Comment on lines +70 to +77

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same beep problem as the listen-in sample. A coach that joins with the default beep plays the enter tone to the customer, then the exit tone when the coach leaves.

Suggested change
```yaml title="SWML"
version: 1.0.0
sections:
main:
- join_conference:
name: support-floor
coach: b3877ee3-6f3c-4985-8066-6d24e3f65e12
```
```yaml title="SWML"
version: 1.0.0
sections:
main:
- join_conference:
name: support-floor
beep: "false"
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conference must also have started, not just contain the agent. conference_node_join (new_conference_controller.c) refuses a coach with not_ready_for_coach when the conference doesn't exist yet ("can't create a conference with a coach") or when node->state != CM_CS_STARTED. An agent who is alone and waiting, for example before the customer leg lands, can't be coached yet. Both refusals map to conference_dial_status=busy, so the outcomes you describe are right (DialCallStatus=busy in mod_laml.c's <Conference> loop, join_conference_result: failed with the script continuing in swml_method_join_conference). The REST path has the same gate: conference_node_coach returns not_ready_for_coach before the start, and Rails' 16002 check only looks at the participant row, so that request can return success without coaching starting.

Suggested change
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.
The agent must be in the conference, and the conference must have started, when the supervisor joins. If the call ID in `coach` is not a current participant, or the conference is still waiting for participants, the join fails: in cXML, `<Dial>` completes with a `DialCallStatus` of `busy` and control passes to its action URL; in SWML, `join_conference_result` is `failed` 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't confirm that this works for a conference joined from SWML, and the end-to-end sample relies on it. For a cXML conference the update reaches the member by call SID: Blade::Laml::ConferenceParticipantUpdateSerializer sends call_id: participant.laml_call_id. A conference created by SWML join_conference (or by a Calling API dial with inline SWML, as in the SDK sample) is a Relay conference. For those, API::Laml::ConferenceParticipant::Operations::Update uses Blade::Relay::ConferenceParticipantUpdateSerializer, which sends uuid: participant.id. That's the relay_conference_participants row's own primary key: Kafka::Relay::Event::Calling::Conferencing::Participant::Join creates the row without setting id, and it's the only place that creates one. On the other side, conference_action_conference_participant_update finds the member with conference_node_member_locate(node, segment_id), and that hash is keyed by the call's segment ID. From source, the request would get "Conf member not found" and the supervisor's mode wouldn't change, even though the REST response and the participant record say it did.

I may be missing a mapping, but the PR's testing boxes are unchecked. Before this ships, run the Python sample against a live space and confirm the supervisor's audio actually changes. If it doesn't, either the page scopes mid-call switching to cXML conferences, or it's a backend bug to raise before we document the flow.


| 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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code block title. House style is <Language> — <client>, and the host should be the same placeholder the rest of the page uses. The page mixes example.signalwire.com, your-space.signalwire.com, $PROJECT_ID, and "your-project-id". mdx-capability-guides asks for <YOUR_SPACE>, <YOUR_PROJECT_ID>, <YOUR_API_TOKEN> and so on, explained once in a | Value | Replace with | table before the first code block. No compatibility-API reference example covers coaching, so a cURL block is the right call here.

Suggested change
```bash title="Listen in to whisper"
```bash title="cURL — Compatibility API"

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop this sentence. It's a stray aside ("the only reader this split inconveniences"), and the dial-in-per-mode alternative isn't covered anywhere on the page.


### End to end with the Server SDKs

The Server SDKs place the supervisor's call: [`dial`][sdk-py-dial] on the Calling API accepts inline SWML, so the supervisor leg joins the conference muted without a hosted document. The SDKs have no client for the Compatibility REST API, so the mode switch is one form-encoded HTTP request authenticated with the project ID and API token. The agent's call ID and the conference SID come from the conference's `join` status callback.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prose should describe the operation, not name dial or link the Python reference. Per the house rule, guide prose says "the Server SDKs" and links /docs/server-sdks. Per-language reference links, like [sdk-py-dial] here and the "Server SDK dial" card in Next steps, stay out of prose. Placing the call is also covered by the unified calling guide, so link that rather than re-teaching it.

Suggested change
The Server SDKs place the supervisor's call: [`dial`][sdk-py-dial] on the Calling API accepts inline SWML, so the supervisor leg joins the conference muted without a hosted document. The SDKs have no client for the Compatibility REST API, so the mode switch is one form-encoded HTTP request authenticated with the project ID and API token. The agent's call ID and the conference SID come from the conference's `join` status callback.
The [Server SDKs](/docs/server-sdks) place the supervisor's call over the Calling API with inline SWML, so the supervisor leg joins the conference muted without a hosted document (see [Place a call via SWML](/docs/platform/voice/make-and-receive-calls#place-a-call-via-swml)). The SDKs have no client for the Compatibility REST API, so the mode switch is one form-encoded HTTP request authenticated with the project ID and API token. The agent's call ID and the conference SID come from the conference's `join` status callback.


<CodeBlocks>
```python title="Python" {15-22,36,39}
import requests
from signalwire.rest import RestClient

PROJECT_ID = "your-project-id"
API_TOKEN = "your-api-token"
SPACE = "your-space.signalwire.com"

client = RestClient(project=PROJECT_ID, token=API_TOKEN, host=SPACE)

# 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}}]},
},
)
Comment on lines +130 to +137

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add "beep": "false" here too (see the listen-in comment). Two smaller things on this sample: it uses requests, so the header needs an install line for both packages, and the title should be Python — REST client.

Suggested change
supervisor = client.calling.dial(
from_="+15551234567",
to="+15559876543",
swml={
"version": "1.0.0",
"sections": {"main": [{"join_conference": {"name": "support-floor", "muted": True}}]},
},
)
supervisor = client.calling.dial(
from_="+15551234567",
to="+15559876543",
swml={
"version": "1.0.0",
"sections": {"main": [{"join_conference": {"name": "support-floor", "muted": True, "beep": "false"}}]},
},
)

supervisor_call_id = supervisor["id"]

participant_url = (
f"https://{SPACE}/api/laml/2010-04-01/Accounts/{PROJECT_ID}"
f"/Conferences/{conference_sid}/Participants/{supervisor_call_id}"
)

def set_mode(**fields):
response = requests.post(participant_url, auth=(PROJECT_ID, API_TOKEN), data=fields)
response.raise_for_status()
return response.json()

# 2. Whisper: unmute and coach the agent in one request.
set_mode(Muted="false", Coaching="true", CallSidToCoach=agent_call_id)

# 3. Barge: stop coaching, and everyone hears the supervisor.
set_mode(Coaching="false")
```

```typescript title="TypeScript" {15-22,43,46}
// Install: npm install @signalwire/sdk@2.0.5

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unpin, and switch to the current dial signature. We're moving guides to the latest Server SDK with no pins and no version notes (#908). With today's @signalwire/sdk (3.7.1), this sample doesn't compile. Calling.dial is dial(from: string, to: string, options?) in dist/rest/namespaces/calling.resources.generated.d.ts, and tsc --strict fails at line 172 with TS2554: Expected 2-4 arguments, but got 1. I ran the corrected version below through tsc --strict against 3.7.1 and it compiles. With that change, the version <Note> at lines 207-209 can go.

Suggested change
// Install: npm install @signalwire/sdk@2.0.5
// Install: npm install @signalwire/sdk

import { RestClient } from "@signalwire/sdk";

const PROJECT_ID = "your-project-id";
const API_TOKEN = "your-api-token";
const SPACE = "your-space.signalwire.com";

const client = new RestClient({ project: PROJECT_ID, token: API_TOKEN, host: SPACE });

// 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 } }] },
},
});
Comment on lines +172 to +179

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 supervisor = await client.calling.dial("+15551234567", "+15559876543", {
swml: {
version: "1.0.0",
sections: { main: [{ join_conference: { name: "support-floor", muted: true, beep: "false" } }] },
},
});

const supervisorCallId = supervisor.id;

const participantUrl =
`https://${SPACE}/api/laml/2010-04-01/Accounts/${PROJECT_ID}` +
`/Conferences/${conferenceSid}/Participants/${supervisorCallId}`;

async function setMode(fields: Record<string, string>) {
const response = await fetch(participantUrl, {
method: "POST",
headers: {
Authorization: "Basic " + Buffer.from(`${PROJECT_ID}:${API_TOKEN}`).toString("base64"),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams(fields),
});
if (!response.ok) throw new Error(`Participant update failed: ${response.status}`);
return response.json();
}

// 2. Whisper: unmute and coach the agent in one request.
await setMode({ Muted: "false", Coaching: "true", CallSidToCoach: agentCallId });

// 3. Barge: stop coaching, and everyone hears the supervisor.
await setMode({ Coaching: "false" });
```
</CodeBlocks>

<Note title="TypeScript `dial()` signature depends on the SDK version">
The TypeScript sample pins `@signalwire/sdk@2.0.5`, whose `dial()` takes a single options object. The [TypeScript `dial` reference][sdk-ts-dial] documents the positional `dial(from, to, options)` form of a newer release. Match the form to the version you install.
</Note>
Comment on lines +207 to +209

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this note once the sample uses the current signature. Pin-dependent notes are what we're removing in #908.


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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A coaching switch doesn't arrive on any of the events listed here. coach-start/coach-stop fire conference::participant-coach/participant-coach-stop. new_conference_controller.c maps both to participant-modify and forwards them only when status_callback_event contains modify. cXML's statusCallbackEvent has no modify, so a cXML console never hears about a coaching change made over REST. (Laml::ConferenceStatusCallbackSerializer and the cXML event table on main list no participant-modify.) The relay-format SWML payload also carries the coached call as call_id_to_coach, which is how the join_conference reference on main names it.

Suggested change
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.
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, and include `modify`: a coaching change made over REST arrives only as a `participant-modify` event. In cXML, set `statusCallback` and `statusCallbackEvent` on `<Conference>`; cXML has no event for a coaching change, so read the participant record after each switch. The `join`, `leave`, `mute`, `hold`, and `speaker` events carry each participant's mute and coaching state: `muted`, `coaching`, and `call_id_to_coach` in SWML callbacks, and `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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **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.
- **The agent must be present first, and the conference must have started.** A `coach` that names a call outside the conference, or a coach who joins before the conference starts, fails the join. A REST update naming a call outside the conference 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 dial" href="/docs/server-sdks/reference/python/rest/calling/dial" icon="regular server">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Point this card at something every SDK reader can use. The house rule keeps per-language reference pages out of guide links. Swap this card for the unified calling guide (/docs/platform/voice/make-and-receive-calls#place-a-call-via-swml) or drop it.

Place the supervisor's call with inline SWML from Python, with a TypeScript twin.
</Card>
</CardGroup>
Loading
Loading