diff --git a/fern/assets/images/img/machine-detection-flow-themed.svg b/fern/assets/images/img/machine-detection-flow-themed.svg new file mode 100644 index 0000000000..da9288def9 --- /dev/null +++ b/fern/assets/images/img/machine-detection-flow-themed.svg @@ -0,0 +1,94 @@ + + Machine detection on an outbound call + Your code places a call with detection enabled. SignalWire rings the destination, the destination answers, and SignalWire listens for the first voice activity. A short greeting followed by silence produces a HUMAN result and your code plays the live message. A long greeting produces a MACHINE result; with detect_message_end set, SignalWire keeps listening through the greeting and beep, reports READY, and your code leaves the voicemail message. + + + + + + + + + + + + + Your code + + + + + SignalWire + + + + + Destination + + + dial + detection enabled + + + + rings + + answers + + + + Detector listens + + + + Short greeting, then silence + HUMAN + + + Play the live message + + + + Long greeting + MACHINE + + greeting and beep finish + READY + + + Leave the voicemail + + diff --git a/fern/llms.txt b/fern/llms.txt index d05ee2bcc8..1f839eb300 100644 --- a/fern/llms.txt +++ b/fern/llms.txt @@ -145,7 +145,7 @@ Feature pages are grouped by channel. SDK pages come first when available, follo - [Conferencing](/docs/server-sdks/reference/python/relay/call/join-conference): Join a call to an ad-hoc audio conference. - [Secure payments](/docs/server-sdks/reference/python/relay/call/pay): Collect payment information on a call. - [Collect caller input](/docs/server-sdks/reference/python/relay/call/play-and-collect): Play audio and collect speech or key presses. -- [Answering machine detection](/docs/server-sdks/reference/python/relay/call/detect): Detect answering machines, fax tones, or digits on a call. +- [Machine detection](/docs/platform/voice/machine-detection): Detect whether a person, voicemail, or fax machine is on a call you place or receive, then leave a message, hand off to an AI agent, or send or receive a fax. - [Noise reduction](/docs/server-sdks/reference/python/relay/call/denoise): Start noise reduction on a call. - [Call queues](/docs/server-sdks/reference/python/relay/call/queue-enter): Place a call into a named queue. - [Text-to-speech voices](/docs/platform/voice/tts): Supported providers, voices, and languages. diff --git a/fern/products/platform/pages/calling/voice/machine-detection.mdx b/fern/products/platform/pages/calling/voice/machine-detection.mdx new file mode 100644 index 0000000000..da39f96ce3 --- /dev/null +++ b/fern/products/platform/pages/calling/voice/machine-detection.mdx @@ -0,0 +1,2029 @@ +--- +title: Machine detection +slug: /voice/machine-detection +description: Detect whether a person, voicemail, or fax machine is on a call you place or receive, then leave a message, hand off to an AI agent, or send or receive a fax. +max-toc-depth: 3 +--- + +[outbound-calling]: /docs/platform/voice/outbound-calling +[api-credentials]: /docs/platform/your-signalwire-api-space +[caller-id]: /docs/platform/voice/how-to-set-caller-id-or-cnam +[tcpa]: /docs/platform/compliance/tcpa +[webhooks]: /docs/platform/webhooks +[inbound-voicemail]: /docs/swml/guides/voicemail +[swml-detect-machine]: /docs/swml/reference/calling/detect-machine +[swml-switch]: /docs/swml/reference/calling/switch +[swml-cond]: /docs/swml/reference/calling/cond +[swml-ai]: /docs/swml/reference/calling/ai +[swml-send-fax]: /docs/swml/reference/calling/send-fax +[swml-receive-fax]: /docs/swml/reference/calling/receive-fax +[swml-answer]: /docs/swml/reference/calling/answer +[make-and-receive-calls]: /docs/swml/guides/make-and-receive-calls +[swml-deployment]: /docs/swml/guides/deployment +[py-relay-receive-fax]: /docs/server-sdks/reference/python/relay/call/receive-fax +[ts-relay-receive-fax]: /docs/server-sdks/reference/typescript/relay/call/receive-fax +[ai-best-practices]: /docs/platform/ai/best-practices +[call-commands]: /docs/apis/rest/calls/call-commands +[py-rest-detect]: /docs/server-sdks/reference/python/rest/calling/detect +[ts-rest-detect]: /docs/server-sdks/reference/typescript/rest/calling/detect +[py-relay-detect]: /docs/server-sdks/reference/python/relay/call/detect +[ts-relay-detect]: /docs/server-sdks/reference/typescript/relay/call/detect +[py-relay-amd]: /docs/server-sdks/reference/python/relay/call/detect-answering-machine +[py-relay-fax]: /docs/server-sdks/reference/python/relay/call/detect-fax +[py-relay-send-fax]: /docs/server-sdks/reference/python/relay/call/send-fax +[ts-relay-send-fax]: /docs/server-sdks/reference/typescript/relay/call/send-fax +[py-detect-action]: /docs/server-sdks/reference/python/relay/actions/detect-action +[ts-detect-action]: /docs/server-sdks/reference/typescript/relay/actions/detect-action +[py-relay-events]: /docs/server-sdks/reference/python/relay/events +[ts-relay-events]: /docs/server-sdks/reference/typescript/relay/events + +Find out whether a person, an answering machine, or a fax machine is on the other end of a call, +and choose what the call does next. Start by calling your own phone and hanging up on your +voicemail, then leave a message after the beep, hand a live answer to an AI agent, send or receive +a fax, or screen the calls your number receives. + +## Prepare for detection + +Have these values ready: + +- An outbound call that already works. The [outbound calling guide][outbound-calling] covers + credentials, caller ID, and destinations; this guide adds detection to that call. +- Your Space URL, such as `.signalwire.com`. +- Your Project ID and API token from the Dashboard's [API credentials][api-credentials] page. + Enable the token's **Voice** permission for the Calling API. +- A voice-capable phone number in your Space or a [verified caller ID][caller-id]. +- A destination phone you can answer *and* send to voicemail. Declining the call is the quickest + way to reach the greeting. +- For the inbound examples, a phone number in your Space that runs a SWML script or reaches your + Relay client. The [make and receive calls guide][make-and-receive-calls] shows both setups. + + +Prerecorded and synthesized voicemail messages fall under consent, do-not-call, and calling-hour +rules. Read the [TCPA guide][tcpa] before you dial anyone but yourself. + + +This guide detects who is on a call. To record messages that callers leave on *your* number, see +the [voicemail recipe][inbound-voicemail]. + +## How detection works + +Detection starts when the destination answers. SignalWire listens for the first voice activity, +then classifies what it hears: + +- **Human.** A short greeting followed by silence, the way a person says "Hello?" and waits. +- **Machine.** Continuous speech longer than a threshold, or a single utterance with more words + than a person's greeting usually has. Voicemail greetings and IVR menus both land here. +- **Fax.** A fax tone instead of speech. +- **Unknown.** Nothing heard before the initial timeout, or the overall timeout expired first. + +The result is available as soon as the classification is made. With `detect_message_end` +enabled, detection keeps listening after a machine result until the greeting ends and the beep +sounds, then reports `READY`. That is the moment to start a voicemail message. + + + +Your code places a call with detection enabled. The destination answers and SignalWire listens. If a person speaks briefly and pauses, SignalWire reports HUMAN and your code plays the live message. If a greeting runs long, SignalWire reports MACHINE, keeps listening when detect_message_end is set, reports READY after the beep, and your code leaves a voicemail. + + + + + +```mermaid +sequenceDiagram + participant App as Your code + participant SW as SignalWire + participant Dest as Destination + + App->>SW: dial with detection enabled + SW->>Dest: rings + Dest->>SW: answers + Note over SW,Dest: Detector listens for first voice activity + alt Short greeting, then silence + SW-->>App: HUMAN + Note over App,SW: Play the live message or start an AI agent + else Long greeting or many words + SW-->>App: MACHINE + Note over SW,Dest: With detect_message_end, keeps listening through the greeting and beep + SW-->>App: READY + Note over App,SW: Leave the voicemail message + else Fax tone + SW-->>App: fax + Note over App,SW: Send a fax or hang up + else Nothing heard before initial_timeout + SW-->>App: UNKNOWN + end +``` + + + +Each outcome surfaces in two forms: a lowercase value in the SWML `detect_result` variable, and an +uppercase event in status callbacks and Relay events. + +| `detect_result` | Event | What SignalWire heard | Typical action | +|---|---|---|---| +| `human` | `HUMAN` | A short greeting, then silence | Play your message, start an AI agent, or connect an agent | +| `machine` | `MACHINE` | Speech longer than the voice threshold, or more words than the words threshold | Hang up, or wait for `READY` and leave a message | +| `machine` | `READY` | The greeting ended and the beep sounded (only with `detect_message_end`) | Play the voicemail message | +| `fax` | (fax event) | A fax tone | Send a fax, or hang up | +| `unknown` | `UNKNOWN` | No voice before `initial_timeout` | Treat as a person, or retry later | +| `detecting` | `NOT_READY` | Detection is still running | Wait | +| `error` | `finished` with no result | The detector stopped without a classification | Log it and choose a default | + +`READY`, `NOT_READY`, and `finished` are lifecycle markers. They appear in callbacks and Relay +events but are never assigned to `detect_result`. Fax results report `detect.type` as `fax`; the +[`detect_machine` reference][swml-detect-machine] lists every field. + +### Outbound and inbound calls + +The detector is the same in both directions. What changes is who it listens to, when it starts, +and which fax tone to expect. + +| | Outbound call you place | Inbound call your number receives | +|---|---|---| +| Who is classified | The party that answered | The caller | +| When detection starts | After the destination answers | After you answer, with SWML [`answer`][swml-answer] or Relay `answer()` | +| Where the instructions live | Inline `swml` in the `dial` request, or your Relay code after `dial()` | The SWML script or URL assigned to the number, or your Relay `on_call` handler | +| Fax tone to detect | `CED`, which the answering fax machine sends (the default `tone`) | `CNG`, which the calling fax machine sends; set `tone: "CNG"` | +| What silence means | The greeting hasn't started; let `initial_timeout` run | A person waiting for your greeting; keep `initial_timeout` short and treat `unknown` as a person | +| `detect_message_end` | Waits for the greeting and beep before you leave a message | Rarely useful; callers don't play greetings | +| Typical next step | Leave a message, start an AI agent, or send a fax | Receive a fax, turn away automated callers, or greet the person | + +REST `calling.detect` behaves the same in both directions: once the call is `answered`, send the +command by call ID and read the result at `status_url`. + +## Detect who answered + +Call a phone you can answer, greet the person, and hang up on the voicemail. + + + +### Choose how to detect + +Choose the approach that fits how you want to act on the result. Detection runs on the server: +the Browser SDK can't start it, so each approach runs from your backend or from SWML that +SignalWire executes for you. + +| What you want to do | Where to start | +|---|---| +| Detect in the same request that places the call, and branch inside the SWML document | [SWML `detect_machine`](#start-detection) in a REST `dial`, using cURL or a Server SDK | +| Add detection to a call that is already answered, from any process that knows the call ID | [REST `calling.detect`](#detect-on-a-call-already-in-progress) | +| Decide what happens next in your own code as results arrive | [WebSocket (Relay)](#start-detection), using a Server SDK | +| Screen the calls your number receives, or route incoming faxes | [Detect on an inbound call](#detect-on-an-inbound-call) with SWML or Relay | + +Each approach runs the same detector, but they differ in where you branch on the result and how +you receive it. + +| Function | SWML | REST `calling.detect` | WebSocket (Relay) | +|---|---|---|---| +| Start detection in the request that places the call | | | | +| Add detection to a call that is already answered | | | | +| Run on a call your number receives | | | | +| Branch without writing code that runs during the call | | | | +| Receive results as HTTP callbacks to a URL you host | | | | +| Receive results as events in your own process, with no public URL | | | | +| Stop detection early | | | | +| Detect fax tones | | | | + +### Set your credentials and destination + +Replace these values in the code sample you choose: + +| Value | Replace with | +|---|---| +| `` | Your Space's subdomain in `.signalwire.com` | +| `` | Your Project ID | +| `` | Your API token | +| `` | Your caller ID number | +| `` | The phone number you'll answer, in E.164 format | + +### Start detection + + + + +The `dial` request carries a SignalWire Markup Language (SWML) document. Its first instruction, +[`detect_machine`][swml-detect-machine], blocks until a result is ready, then +[`switch`][swml-switch] reads `detect_result` and picks a branch. Only the `amd` detector runs +here; the fax example below adds the second one. + + + +```bash +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "", + "to": "", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "detect_machine": { + "detectors": "amd", + "timeout": 30 + } + }, + { + "switch": { + "variable": "detect_result", + "case": { + "human": [ + { "play": {"url": "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."} } + ], + "machine": [ + { "hangup": {} } + ] + }, + "default": [ + { "play": {"url": "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."} } + ] + } + }, + { "hangup": {} } + ] + } + } + } + }' +``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as detect_machine.py and run: python detect_machine.py +from signalwire import SWMLBuilder, SWMLService +from signalwire.rest import RestClient + +LIVE = "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow." + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +swml = ( + SWMLBuilder(SWMLService(name="detect-machine")) + .detect_machine(detectors="amd", timeout=30) + .switch( + variable="detect_result", + case={ + "human": [{"play": {"url": LIVE}}], + "machine": [{"hangup": {}}], + }, + default=[{"play": {"url": LIVE}}], + ) + .hangup() + .build() +) + +call = client.calling.dial( + from_="", + to="", + swml=swml, +) +print(call["id"]) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as detect-machine.mjs, +// then run: node detect-machine.mjs +import { RestClient, SwmlBuilder } from "@signalwire/sdk"; + +const LIVE = "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +const swml = new SwmlBuilder() + .detect_machine({ detectors: "amd", timeout: 30 }) + .switch({ + variable: "detect_result", + case: { + human: [{ play: { url: LIVE } }], + machine: [{ hangup: {} }], + }, + default: [{ play: { url: LIVE } }], + }) + .hangup() + .build(); + +const call = await client.calling.dial({ + from: "", + to: "", + swml, +}); +console.log(call.id); +``` + + + +The `default` branch catches `unknown` and `error`, so a quiet person still hears the message. +Prefer [`cond`][swml-cond] when a branch needs a compound condition, such as checking +`detect_machine_beep` as well. + + + + +Relay's `dial()` has no detection option, so detection is a separate step once the call is +answered. In Python, [`detect_answering_machine()`][py-relay-amd] wraps the `machine` detector; +in TypeScript, pass the detector to [`detect()`][ts-relay-detect] directly. Both return a +`DetectAction` ([Python][py-detect-action], [TypeScript][ts-detect-action]) whose `wait()` +resolves on the first result. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as detect_machine.py and run: python detect_machine.py +import asyncio +from signalwire.relay import RelayClient + +LIVE = "Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow." + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + +def outcome(event) -> str: + return event.params.get("detect", {}).get("params", {}).get("event", "") + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "", + "to_number": "", + "timeout": 30, + }, + }]], + ) + action = await call.detect_answering_machine(timeout=30) + result = outcome(await action.wait()) + print(f"Detected: {result}") + + if result == "MACHINE": + await call.hangup() + return + + # HUMAN, or UNKNOWN when nobody spoke in time. + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play( + [{"type": "tts", "params": {"text": LIVE}}], + on_completed=hang_up_after_playback, + ) + await call.wait_for_ended() + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as detect-machine.mts and run: npx tsx detect-machine.mts +import { RelayClient, RelayEvent } from "@signalwire/sdk"; + +const LIVE = "Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +function outcome(event: RelayEvent): string { + const detect = event.params.detect as { params?: { event?: string } } | undefined; + return detect?.params?.event ?? ""; +} + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "", + to_number: "", + timeout: 30, + }, + }]]); + const action = await call.detect({ type: "machine", params: {} }, { timeout: 30 }); + const result = outcome(await action.wait()); + console.log(`Detected: ${result}`); + + if (result === "MACHINE") { + await call.hangup(); + } else { + // HUMAN, or UNKNOWN when nobody spoke in time. + await call.play([{ type: "tts", text: LIVE }], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + await call.waitForEnded(); + } +} finally { + await client.disconnect(); +} +``` + + + +In Python, the raw [`detect()`][py-relay-detect] method takes the same parameters inside a +`{"type": "machine", "params": {...}}` object. Both SDKs use that shape for the `fax` and `digit` +detectors. + + + + +### Answer the call, then decline it + +Answer the call and say "Hello?". After a short pause you hear the ride confirmation, then the +call ends. Run the sample again and decline the call so it reaches voicemail: the call ends a few +seconds into your greeting and leaves no message. With Relay, the console prints `Detected: MACHINE`. + +If the second run plays the message into your voicemail, the greeting was classified as `human` +or `unknown`. A greeting that pauses after "Hi, you've reached…" looks like a person, and one that +opens with silence longer than `initial_timeout` returns `unknown`. If the first run hangs up on +you, you spoke past the voice threshold without pausing. The [tuning table](#tune-detection) maps +each symptom to its setting. + + + +## Track detection events + +Every detection result is also an event named `calling.call.detect`. Receive it as an HTTP +callback with REST, or as a Relay event in your own process. + +### Track detection events via REST + +Add `status_url` to `detect_machine` and SignalWire posts each detector event to that URL as +JSON. The `params.detect.params.event` field carries the value, and `beep` appears only when a +beep was heard. Point `` at an endpoint you control; the +[webhooks guide][webhooks] covers setup and local testing. + +```json +{ + "event_type": "calling.call.detect", + "event_channel": "swml:be38xxxx-8xxx-4xxxx-9fxx-bxxxxxxxxx", + "timestamp": 1745332535.668522, + "project_id": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "space_id": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "params": { + "control_id": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "detect": { + "type": "machine", + "params": { + "event": "MACHINE", + "beep": true + } + }, + "call_id": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "node_id": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "segment_id": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } +} +``` + +By default `detect_machine` waits for a result before the next SWML instruction runs. Set +`wait: false` to keep the document moving while detection runs in the background, and handle +the outcome at `status_url` instead. Asynchronous detection requires `status_url`. + + + +```yaml +version: 1.0.0 +sections: + main: + - detect_machine: + detectors: amd + wait: false + status_url: "" + - play: + url: "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow." +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { + "detect_machine": { + "detectors": "amd", + "wait": false, + "status_url": "" + } + }, + { "play": {"url": "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."} } + ] + } +} +``` + + + +The REST [`calling.detect`](#detect-on-a-call-already-in-progress) command delivers its results +the same way, through its own `status_url`. + +### Track detection events via WebSocket + +Register a handler for `calling.call.detect` before starting detection. Each event arrives as a +`DetectEvent` ([Python][py-relay-events], [TypeScript][ts-relay-events]) whose `detect` property +holds the same `type` and `params` object as the callback payload. Unlike `wait()`, which returns once, the handler sees every event: +`MACHINE` while the greeting is still playing, `READY` after the beep, and `finished` when the +detector stops. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as detect_events.py and run: python detect_events.py +import asyncio +from signalwire.relay import RelayClient +from signalwire.relay.event import DetectEvent + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "", + "to_number": "", + "timeout": 30, + }, + }]], + ) + def log_detect(event: DetectEvent): + params = event.detect.get("params", {}) + print(f"{event.detect.get('type')}: {params.get('event')} beep={params.get('beep', False)}") + + call.on("calling.call.detect", log_detect) + action = await call.detect_answering_machine(detect_message_end=True, timeout=30) + await action.wait() + await asyncio.sleep(20) # Keep listening for READY and finished. + await call.hangup() + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as detect-events.mts and run: npx tsx detect-events.mts +import { RelayClient, DetectEvent } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "", + to_number: "", + timeout: 30, + }, + }]]); + call.on("calling.call.detect", (event) => { + const detect = (event as DetectEvent).detect as { + type?: string; + params?: { event?: string; beep?: boolean }; + }; + console.log(`${detect.type}: ${detect.params?.event} beep=${detect.params?.beep ?? false}`); + }); + const action = await call.detect( + { type: "machine", params: { detect_message_end: true } }, + { timeout: 30 }, + ); + await action.wait(); + await new Promise((resolve) => setTimeout(resolve, 20_000)); // Keep listening for READY and finished. + await call.hangup(); +} finally { + await client.disconnect(); +} +``` + + + +### Compare the surfaces + +SWML, the REST command, and Relay share one detector but expose it differently. + +| Setting | SWML `detect_machine` | REST `calling.detect` | Relay `detect()` | +|---|---|---|---| +| `detect_message_end` default | `false` | `true` | Not sent unless you set it | +| Which detectors run | `detectors`, a comma-separated list of `amd` and `fax` (default both) | One `detect.type` per command: `machine`, `fax`, or `digit` | One `detect.type` per call; Python adds `detect_answering_machine()`, `detect_fax()`, and `detect_digit()` helpers | +| Blocking behavior | `wait` (default `true`) pauses the document | Returns immediately; results go to `status_url` | `wait()` resolves on the first result; events continue | +| `control_id` | None | Required, and the only way to stop it | Optional; the action's `stop()` uses it | +| Fax `tone` values | `CED` or `CNG` | `CED`, `CNG`, `ced`, or `cng`; omit for either | `CED` or `CNG` | +| `machine_ready_timeout` | Yes | Yes | Not on the Python helper; pass it to `detect()` | +| `detect_interruptions` | No | Yes | Yes | + +## Examples + +### Leave a message after the beep + +Wait for the voicemail greeting and beep to finish, then leave a message. Without +`detect_message_end`, a `machine` result arrives while the greeting is still playing, and anything +you say then is lost. The [outbound calling guide][outbound-calling] has a shorter version of this flow. + +#### Leave a message via REST + +Set `detect_message_end` so `detect_machine` returns after the beep. The `machine` branch then +plays into the recording. `detect_machine_beep` records whether a beep was heard, and +`detect_ms` how long detection took. + + + +```bash +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "", + "to": "", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "detect_machine": { + "detectors": "amd", + "detect_message_end": true, + "timeout": 45 + } + }, + { + "switch": { + "variable": "detect_result", + "case": { + "human": [ + { "play": {"url": "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."} } + ], + "machine": [ + { "play": {"url": "say:This is Bayview Taxi. Your ride is confirmed for 8 AM tomorrow. Reply to our text message if you need to change it."} } + ] + }, + "default": [ + { "play": {"url": "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."} } + ] + } + }, + { "hangup": {} } + ] + } + } + } + }' +``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as leave_message.py and run: python leave_message.py +from signalwire import SWMLBuilder, SWMLService +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +LIVE = "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow." +VOICEMAIL = ( + "say:This is Bayview Taxi. Your ride is confirmed for 8 AM tomorrow. " + "Reply to our text message if you need to change it." +) + +swml = ( + SWMLBuilder(SWMLService(name="leave-a-message")) + .detect_machine(detectors="amd", detect_message_end=True, timeout=45) + .switch( + variable="detect_result", + case={ + "human": [{"play": {"url": LIVE}}], + "machine": [{"play": {"url": VOICEMAIL}}], + }, + default=[{"play": {"url": LIVE}}], + ) + .hangup() + .build() +) + +call = client.calling.dial( + from_="", + to="", + swml=swml, +) +print(call["id"]) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as leave-message.mjs, +// then run: node leave-message.mjs +import { RestClient, SwmlBuilder } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +const LIVE = "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."; +const VOICEMAIL = + "say:This is Bayview Taxi. Your ride is confirmed for 8 AM tomorrow. " + + "Reply to our text message if you need to change it."; + +const swml = new SwmlBuilder() + .detect_machine({ detectors: "amd", detect_message_end: true, timeout: 45 }) + .switch({ + variable: "detect_result", + case: { + human: [{ play: { url: LIVE } }], + machine: [{ play: { url: VOICEMAIL } }], + }, + default: [{ play: { url: LIVE } }], + }) + .hangup() + .build(); + +const call = await client.calling.dial({ + from: "", + to: "", + swml, +}); +console.log(call.id); +``` + + + +The `timeout` is longer here because it now covers the whole greeting. `machine_ready_timeout` +sets how much silence after the greeting counts as "ready"; it defaults to +`end_silence_timeout`. Raise it for greetings that pause before the beep. + +#### Leave a message via WebSocket (Relay) + +Use the event handler rather than `wait()`. With `detect_message_end` on, a `MACHINE` event +arrives first, while the greeting is still playing; `READY` follows after the beep. Play the +voicemail on `READY`, and the live message on `HUMAN` or `UNKNOWN`. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as leave_message.py and run: python leave_message.py +import asyncio +from signalwire.relay import RelayClient +from signalwire.relay.event import DetectEvent + +LIVE = "Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow." +VOICEMAIL = ( + "This is Bayview Taxi. Your ride is confirmed for 8 AM tomorrow. " + "Reply to our text message if you need to change it." +) + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "", + "to_number": "", + "timeout": 30, + }, + }]], + ) + announced = False + + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + async def speak(text: str): + nonlocal announced + announced = True + await call.play( + [{"type": "tts", "params": {"text": text}}], + on_completed=hang_up_after_playback, + ) + + async def on_detect(event: DetectEvent): + if announced or call.state == "ended": + return + result = event.detect.get("params", {}).get("event", "") + if result == "READY": + # The greeting and its beep have finished. + await speak(VOICEMAIL) + elif result in ("HUMAN", "UNKNOWN"): + await speak(LIVE) + elif result == "finished": + # Detection timed out without a usable result. + await call.hangup() + + call.on("calling.call.detect", on_detect) + await call.detect_answering_machine(detect_message_end=True, timeout=45) + await call.wait_for_ended() + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as leave-message.mts and run: npx tsx leave-message.mts +import { RelayClient, DetectEvent } from "@signalwire/sdk"; + +const LIVE = "Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."; +const VOICEMAIL = + "This is Bayview Taxi. Your ride is confirmed for 8 AM tomorrow. " + + "Reply to our text message if you need to change it."; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "", + to_number: "", + timeout: 30, + }, + }]]); + let announced = false; + + const speak = async (text: string) => { + announced = true; + await call.play([{ type: "tts", text }], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + }; + + call.on("calling.call.detect", async (event) => { + if (announced || call.state === "ended") return; + const detect = (event as DetectEvent).detect as { params?: { event?: string } }; + const result = detect.params?.event ?? ""; + if (result === "READY") { + // The greeting and its beep have finished. + await speak(VOICEMAIL); + } else if (result === "HUMAN" || result === "UNKNOWN") { + await speak(LIVE); + } else if (result === "finished") { + // Detection timed out without a usable result. + await call.hangup(); + } + }); + + await call.detect({ type: "machine", params: { detect_message_end: true } }, { timeout: 45 }); + await call.waitForEnded(); +} finally { + await client.disconnect(); +} +``` + + + +### Hand a live answer to an AI agent + +Start an AI agent when a person answers, and leave a voicemail otherwise. + + +Before dialing, follow consent, do-not-call, and calling-hour requirements for artificial voices. +See the [TCPA guide][tcpa] and [AI best practices][ai-best-practices]. + + +#### Hand off to an AI agent via REST + +Put the [`ai`][swml-ai] instruction in the `human` branch and the voicemail message in the +`machine` branch. + + + +```bash +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "", + "to": "", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "detect_machine": { + "detectors": "amd", + "detect_message_end": true, + "timeout": 45 + } + }, + { + "switch": { + "variable": "detect_result", + "case": { + "human": [ + { + "ai": { + "params": { + "static_greeting": "Hi, this is Ada from Bayview Taxi calling about your ride tomorrow. This call uses an artificial voice.", + "static_greeting_no_barge": true + }, + "prompt": { + "text": "You are Ada, a dispatcher for Bayview Taxi. Confirm the rider still wants their 8 AM pickup tomorrow. If they want to change the time, take the new time and repeat it back. Keep answers short." + } + } + } + ], + "machine": [ + { "play": {"url": "say:This is Bayview Taxi confirming your 8 AM ride tomorrow. Call us back if anything changes."} }, + { "hangup": {} } + ] + }, + "default": [ + { "play": {"url": "say:This is Bayview Taxi confirming your 8 AM ride tomorrow. Call us back if anything changes."} }, + { "hangup": {} } + ] + } + } + ] + } + } + } + }' +``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as detect_then_ai.py and run: python detect_then_ai.py +from signalwire import SWMLBuilder, SWMLService +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +VOICEMAIL = "say:This is Bayview Taxi confirming your 8 AM ride tomorrow. Call us back if anything changes." +AGENT = { + "ai": { + "params": { + "static_greeting": "Hi, this is Ada from Bayview Taxi calling about your ride tomorrow. This call uses an artificial voice.", + "static_greeting_no_barge": True, + }, + "prompt": { + "text": ( + "You are Ada, a dispatcher for Bayview Taxi. Confirm the rider still wants " + "their 8 AM pickup tomorrow. If they want to change the time, take the new " + "time and repeat it back. Keep answers short." + ) + }, + } +} + +swml = ( + SWMLBuilder(SWMLService(name="detect-then-ai")) + .detect_machine(detectors="amd", detect_message_end=True, timeout=45) + .switch( + variable="detect_result", + case={ + "human": [AGENT], + "machine": [{"play": {"url": VOICEMAIL}}, {"hangup": {}}], + }, + default=[{"play": {"url": VOICEMAIL}}, {"hangup": {}}], + ) + .build() +) + +call = client.calling.dial( + from_="", + to="", + swml=swml, +) +print(call["id"]) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as detect-then-ai.mjs, +// then run: node detect-then-ai.mjs +import { RestClient, SwmlBuilder } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +const VOICEMAIL = "say:This is Bayview Taxi confirming your 8 AM ride tomorrow. Call us back if anything changes."; +const AGENT = { + ai: { + params: { + static_greeting: "Hi, this is Ada from Bayview Taxi calling about your ride tomorrow. This call uses an artificial voice.", + static_greeting_no_barge: true, + }, + prompt: { + text: + "You are Ada, a dispatcher for Bayview Taxi. Confirm the rider still wants " + + "their 8 AM pickup tomorrow. If they want to change the time, take the new " + + "time and repeat it back. Keep answers short.", + }, + }, +}; + +const swml = new SwmlBuilder() + .detect_machine({ detectors: "amd", detect_message_end: true, timeout: 45 }) + .switch({ + variable: "detect_result", + case: { + human: [AGENT], + machine: [{ play: { url: VOICEMAIL } }, { hangup: {} }], + }, + default: [{ play: { url: VOICEMAIL } }, { hangup: {} }], + }) + .build(); + +const call = await client.calling.dial({ + from: "", + to: "", + swml, +}); +console.log(call.id); +``` + + + +#### Hand off to an AI agent via WebSocket (Relay) + +Start the agent with `call.ai()` on `HUMAN`, and play the voicemail on `READY`. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as detect_then_ai.py and run: python detect_then_ai.py +import asyncio +from signalwire.relay import RelayClient +from signalwire.relay.event import DetectEvent + +VOICEMAIL = "This is Bayview Taxi confirming your 8 AM ride tomorrow. Call us back if anything changes." + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "", + "to_number": "", + "timeout": 30, + }, + }]], + ) + handled = False + + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + async def on_detect(event: DetectEvent): + nonlocal handled + if handled or call.state == "ended": + return + result = event.detect.get("params", {}).get("event", "") + if result in ("HUMAN", "UNKNOWN"): + handled = True + await call.ai( + ai_params={ + "static_greeting": "Hi, this is Ada from Bayview Taxi calling about your ride tomorrow. This call uses an artificial voice.", + "static_greeting_no_barge": True, + }, + prompt={ + "text": """You are Ada, a dispatcher for Bayview Taxi. Confirm the rider +still wants their 8 AM pickup tomorrow. If they want to change the time, take +the new time and repeat it back. Keep answers short.""" + }, + ) + elif result == "READY": + handled = True + await call.play( + [{"type": "tts", "params": {"text": VOICEMAIL}}], + on_completed=hang_up_after_playback, + ) + elif result == "finished": + await call.hangup() + + call.on("calling.call.detect", on_detect) + await call.detect_answering_machine(detect_message_end=True, timeout=45) + await call.wait_for_ended() + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as detect-then-ai.mts and run: npx tsx detect-then-ai.mts +import { RelayClient, DetectEvent } from "@signalwire/sdk"; + +const VOICEMAIL = "This is Bayview Taxi confirming your 8 AM ride tomorrow. Call us back if anything changes."; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "", + to_number: "", + timeout: 30, + }, + }]]); + let handled = false; + + call.on("calling.call.detect", async (event) => { + if (handled || call.state === "ended") return; + const detect = (event as DetectEvent).detect as { params?: { event?: string } }; + const result = detect.params?.event ?? ""; + if (result === "HUMAN" || result === "UNKNOWN") { + handled = true; + await call.ai({ + aiParams: { + static_greeting: "Hi, this is Ada from Bayview Taxi calling about your ride tomorrow. This call uses an artificial voice.", + static_greeting_no_barge: true, + }, + prompt: { + text: `You are Ada, a dispatcher for Bayview Taxi. Confirm the rider still + wants their 8 AM pickup tomorrow. If they want to change the time, take the + new time and repeat it back. Keep answers short.`, + }, + }); + } else if (result === "READY") { + handled = true; + await call.play([{ type: "tts", text: VOICEMAIL }], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + } else if (result === "finished") { + await call.hangup(); + } + }); + + await call.detect({ type: "machine", params: { detect_message_end: true } }, { timeout: 45 }); + await call.waitForEnded(); +} finally { + await client.disconnect(); +} +``` + + + +### Send a fax when a fax machine answers + +Detect a fax machine on the other end and send it a document, or speak to the person who +answered instead. A fax machine that answers a call sends a `CED` tone, which is the detector's +default `tone`; a fax machine that calls you sends `CNG` instead, covered in +[Receive a fax on an inbound call](#receive-a-fax-on-an-inbound-call). `` +must point at a PDF that SignalWire can fetch. + +#### Send a fax via REST + +Enable both detectors with `detectors: "amd,fax"` and add a `fax` branch that runs +[`send_fax`][swml-send-fax]. + + + +```bash +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "", + "to": "", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "detect_machine": { + "detectors": "amd,fax", + "timeout": 30 + } + }, + { + "switch": { + "variable": "detect_result", + "case": { + "fax": [ + { + "send_fax": { + "document": "", + "header_info": "Bayview Taxi receipt" + } + } + ], + "human": [ + { "play": {"url": "say:Hello, this is Bayview Taxi. We tried to fax your receipt. We will email it instead."} } + ] + }, + "default": [ + { "hangup": {} } + ] + } + }, + { "hangup": {} } + ] + } + } + } + }' +``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as detect_fax.py and run: python detect_fax.py +from signalwire import SWMLBuilder, SWMLService +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +NOT_A_FAX = "say:Hello, this is Bayview Taxi. We tried to fax your receipt. We will email it instead." + +swml = ( + SWMLBuilder(SWMLService(name="detect-fax")) + .detect_machine(detectors="amd,fax", timeout=30) + .switch( + variable="detect_result", + case={ + "fax": [{ + "send_fax": { + "document": "", + "header_info": "Bayview Taxi receipt", + } + }], + "human": [{"play": {"url": NOT_A_FAX}}], + }, + default=[{"hangup": {}}], + ) + .hangup() + .build() +) + +call = client.calling.dial( + from_="", + to="", + swml=swml, +) +print(call["id"]) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as detect-fax.mjs, +// then run: node detect-fax.mjs +import { RestClient, SwmlBuilder } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +const NOT_A_FAX = "say:Hello, this is Bayview Taxi. We tried to fax your receipt. We will email it instead."; + +const swml = new SwmlBuilder() + .detect_machine({ detectors: "amd,fax", timeout: 30 }) + .switch({ + variable: "detect_result", + case: { + fax: [{ + send_fax: { + document: "", + header_info: "Bayview Taxi receipt", + }, + }], + human: [{ play: { url: NOT_A_FAX } }], + }, + default: [{ hangup: {} }], + }) + .hangup() + .build(); + +const call = await client.calling.dial({ + from: "", + to: "", + swml, +}); +console.log(call.id); +``` + + + +#### Send a fax via WebSocket (Relay) + +Relay runs one detector per operation. The `fax` detector ([`detect_fax()`][py-relay-fax] in +Python, `detect()` with `type: "fax"` in TypeScript) listens only for a fax tone, so give it a +short timeout: a fax machine answers with its tone within a second or two, and a person doesn't. When the action +resolves with a `fax` result, send the document with `send_fax()` ([Python][py-relay-send-fax], +[TypeScript][ts-relay-send-fax]); otherwise treat the answer as a person. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as detect_fax.py and run: python detect_fax.py +import asyncio +from signalwire.relay import RelayClient + +NOT_A_FAX = "Hello, this is Bayview Taxi. We tried to fax your receipt. We will email it instead." + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + +def is_fax(event) -> bool: + detect = event.params.get("detect", {}) + outcome = detect.get("params", {}).get("event", "") + return detect.get("type") == "fax" and outcome not in ("finished", "error", "") + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "", + "to_number": "", + "timeout": 30, + }, + }]], + ) + action = await call.detect_fax(tone="CED", timeout=8) + event = await action.wait() + + if is_fax(event): + fax = await call.send_fax( + document="", + header_info="Bayview Taxi receipt", + ) + result = await fax.wait() + print(f"Fax sent: {result.params.get('fax', {}).get('pages', 0)} pages") + await call.hangup() + return + + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play( + [{"type": "tts", "params": {"text": NOT_A_FAX}}], + on_completed=hang_up_after_playback, + ) + await call.wait_for_ended() + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as detect-fax.mts and run: npx tsx detect-fax.mts +import { RelayClient, RelayEvent } from "@signalwire/sdk"; + +const NOT_A_FAX = "Hello, this is Bayview Taxi. We tried to fax your receipt. We will email it instead."; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +function isFax(event: RelayEvent): boolean { + const detect = event.params.detect as { type?: string; params?: { event?: string } } | undefined; + const outcome = detect?.params?.event ?? ""; + return detect?.type === "fax" && !["finished", "error", ""].includes(outcome); +} + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "", + to_number: "", + timeout: 30, + }, + }]]); + const action = await call.detect({ type: "fax", params: { tone: "CED" } }, { timeout: 8 }); + const event = await action.wait(); + + if (isFax(event)) { + const fax = await call.sendFax("", { + headerInfo: "Bayview Taxi receipt", + }); + const result = await fax.wait(); + const pages = (result.params.fax as { pages?: number } | undefined)?.pages ?? 0; + console.log(`Fax sent: ${pages} pages`); + await call.hangup(); + } else { + await call.play([{ type: "tts", text: NOT_A_FAX }], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + await call.waitForEnded(); + } +} finally { + await client.disconnect(); +} +``` + + + +### Detect on a call already in progress + +Start detection on a call that is already answered, from any process that knows the call ID. This +suits calls placed with a `url` that your server answered, calls that changed hands, such as a leg +you connected to a customer, and inbound calls your server is handling over HTTP. + +Send the [`calling.detect` command][call-commands] to the Calling API with the call's `id`, or +call the REST client's `detect()` method ([Python][py-rest-detect], [TypeScript][ts-rest-detect]). +The command returns at once; the result arrives at `status_url` as the same `calling.call.detect` +payload shown above. `control_id` is required, and it's the only handle for stopping the detector +early with `calling.detect.stop`. Note that this command defaults `detect_message_end` to `true`. + + + +```bash +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "", + "command": "calling.detect", + "params": { + "control_id": "amd-1", + "detect": { + "type": "machine", + "params": { + "detect_message_end": true + } + }, + "timeout": 30, + "status_url": "" + } + }' +``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +# status_url has no keyword argument yet; extras passes it through as-is. +client.calling.detect( + call_id="", + control_id="amd-1", + detect={"type": "machine", "params": {"detect_message_end": True}}, + timeout=30, + extras={"status_url": ""}, +) + +# Later, if the result no longer matters: +client.calling.detect_stop(call_id="", control_id="amd-1") +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +import { RestClient } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +await client.calling.detect("", { + control_id: "amd-1", + detect: { type: "machine", params: { detect_message_end: true } }, + timeout: 30, + status_url: "", +}); + +// Later, if the result no longer matters: +await client.calling.detectStop("", { control_id: "amd-1" }); +``` + + + +The call must be in the `answered` state. Sending the command to a call that is still ringing +fails. To act on the result, have your webhook handler send another call command by the same +`id`, such as `calling.play` for a voicemail message. + +### Detect on an inbound call + +Run detection on calls your number receives. Two things differ from the outbound flow: you answer +first, and a person who calls you waits for your greeting instead of speaking. Silence from a +person becomes `unknown` once `initial_timeout` expires, so keep that timeout short and treat +`unknown` as a person. + +#### Screen an inbound call via SWML + +Assign this document to your number as a SWML script, or serve it from your own URL; the +[deployment guide][swml-deployment] covers both. [`answer`][swml-answer] runs first, then +`detect_machine` listens for two seconds before the greeting plays. + + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - detect_machine: + detectors: amd + initial_timeout: 2 + timeout: 10 + - switch: + variable: detect_result + case: + machine: + - play: + url: "say:This line doesn't accept automated calls. Goodbye." + - hangup: {} + default: + - play: + url: "say:Thanks for calling Bayview Taxi. A dispatcher will be with you shortly." + - hangup: {} +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { + "detect_machine": { + "detectors": "amd", + "initial_timeout": 2, + "timeout": 10 + } + }, + { + "switch": { + "variable": "detect_result", + "case": { + "machine": [ + { "play": {"url": "say:This line doesn't accept automated calls. Goodbye."} }, + { "hangup": {} } + ] + }, + "default": [ + { "play": {"url": "say:Thanks for calling Bayview Taxi. A dispatcher will be with you shortly."} } + ] + } + }, + { "hangup": {} } + ] + } +} +``` + + + +The `default` branch greets both `human` and `unknown`, so a quiet caller hears the greeting after +a two-second pause. Add `status_url` to `detect_machine` to log each result. + +#### Screen an inbound call via WebSocket (Relay) + +Handle the call in `on_call` (Python) or `onCall` (TypeScript), answer it, then detect exactly as +you would after a dial. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_detect.py and run: python inbound_detect.py +from signalwire.relay import RelayClient + +GREETING = "Thanks for calling Bayview Taxi. A dispatcher will be with you shortly." +REJECT = "This line doesn't accept automated calls. Goodbye." + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + +def outcome(event) -> str: + return event.params.get("detect", {}).get("params", {}).get("event", "") + +@client.on_call +async def handle_call(call): + await call.answer() + action = await call.detect_answering_machine(initial_timeout=2, timeout=10) + result = outcome(await action.wait()) + print(f"Detected: {result}") + # HUMAN, or UNKNOWN when the caller waited for you to speak. + text = REJECT if result == "MACHINE" else GREETING + playback = await call.play([{"type": "tts", "params": {"text": text}}]) + await playback.wait() + await call.hangup() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as inbound-detect.mts and run: npx tsx inbound-detect.mts +import { RelayClient, RelayEvent } from "@signalwire/sdk"; + +const GREETING = "Thanks for calling Bayview Taxi. A dispatcher will be with you shortly."; +const REJECT = "This line doesn't accept automated calls. Goodbye."; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +function outcome(event: RelayEvent): string { + const detect = event.params.detect as { params?: { event?: string } } | undefined; + return detect?.params?.event ?? ""; +} + +client.onCall(async (call) => { + await call.answer(); + const action = await call.detect( + { type: "machine", params: { initial_timeout: 2 } }, + { timeout: 10 }, + ); + const result = outcome(await action.wait()); + console.log(`Detected: ${result}`); + // HUMAN, or UNKNOWN when the caller waited for you to speak. + const text = result === "MACHINE" ? REJECT : GREETING; + const playback = await call.play([{ type: "tts", text }]); + await playback.wait(); + await call.hangup(); +}); + +await client.run(); +``` + + + +Call the number from your phone and stay quiet: after two seconds you hear the greeting. Call it +again and hold the phone up to a speaker playing a long recording, and the call ends with the +rejection message. + +### Receive a fax on an inbound call + +Let one number take both voice calls and faxes. A fax machine that dials you sends a `CNG` tone as +soon as you answer, so listen for that tone and hand the call to `receive_fax` when it appears. +Keep the timeout short so voice callers aren't left in silence. + +#### Receive a fax via SWML + +Set `detectors` to `fax` and `tone` to `CNG`. A `fax` result runs +[`receive_fax`][swml-receive-fax]; anything else greets the caller. + + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - detect_machine: + detectors: fax + tone: CNG + timeout: 5 + - switch: + variable: detect_result + case: + fax: + - receive_fax: + status_url: "" + default: + - play: + url: "say:Thanks for calling Bayview Taxi. A dispatcher will be with you shortly." + - hangup: {} +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { + "detect_machine": { + "detectors": "fax", + "tone": "CNG", + "timeout": 5 + } + }, + { + "switch": { + "variable": "detect_result", + "case": { + "fax": [ + { "receive_fax": {"status_url": ""} } + ] + }, + "default": [ + { "play": {"url": "say:Thanks for calling Bayview Taxi. A dispatcher will be with you shortly."} } + ] + } + }, + { "hangup": {} } + ] + } +} +``` + + + +`receive_fax` posts the finished document's details to its own `status_url`. + +#### Receive a fax via WebSocket (Relay) + +Detect the `CNG` tone with the `fax` detector, then call `receive_fax()` +([Python][py-relay-receive-fax], [TypeScript][ts-relay-receive-fax]) and wait for the document. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_fax.py and run: python inbound_fax.py +from signalwire.relay import RelayClient + +GREETING = "Thanks for calling Bayview Taxi. A dispatcher will be with you shortly." + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + +def is_fax(event) -> bool: + detect = event.params.get("detect", {}) + outcome = detect.get("params", {}).get("event", "") + return detect.get("type") == "fax" and outcome not in ("finished", "error", "") + +@client.on_call +async def handle_call(call): + await call.answer() + action = await call.detect_fax(tone="CNG", timeout=5) + event = await action.wait() + + if is_fax(event): + fax = await call.receive_fax() + result = await fax.wait() + fax_result = result.params.get("fax", {}) + print(f"Received {fax_result.get('pages', 0)} pages: {fax_result.get('document', '')}") + await call.hangup() + return + + playback = await call.play([{"type": "tts", "params": {"text": GREETING}}]) + await playback.wait() + await call.hangup() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as inbound-fax.mts and run: npx tsx inbound-fax.mts +import { RelayClient, RelayEvent } from "@signalwire/sdk"; + +const GREETING = "Thanks for calling Bayview Taxi. A dispatcher will be with you shortly."; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +function isFax(event: RelayEvent): boolean { + const detect = event.params.detect as { type?: string; params?: { event?: string } } | undefined; + const outcome = detect?.params?.event ?? ""; + return detect?.type === "fax" && !["finished", "error", ""].includes(outcome); +} + +client.onCall(async (call) => { + await call.answer(); + const action = await call.detect({ type: "fax", params: { tone: "CNG" } }, { timeout: 5 }); + const event = await action.wait(); + + if (isFax(event)) { + const fax = await call.receiveFax(); + const result = await fax.wait(); + const faxResult = result.params.fax as { pages?: number; document?: string } | undefined; + console.log(`Received ${faxResult?.pages ?? 0} pages: ${faxResult?.document ?? ""}`); + await call.hangup(); + return; + } + + const playback = await call.play([{ type: "tts", text: GREETING }]); + await playback.wait(); + await call.hangup(); +}); + +await client.run(); +``` + + + +### Tune detection + +The defaults suit most residential voicemail. Adjust them when a run misclassifies, and change +one setting at a time so you can see its effect. Every setting below is a `detect_machine` +property, a `detect.params` field in REST, and a keyword argument on the Relay helper. + +| Symptom | Setting | Default | Change | +|---|---|---|---| +| Greeting begins with silence and comes back `unknown` | `initial_timeout` | 4.5 s | Raise it | +| A person who talks for a while is classified `machine` | `machine_voice_threshold` | 1.25 s | Raise it | +| A short, scripted greeting is classified `human` | `machine_words_threshold` | 6 words | Lower it | +| A person's pause after "Hello?" is too short to count as silence | `end_silence_timeout` | 1.0 s | Lower it | +| The voicemail message starts before the beep | `machine_ready_timeout` | Same as `end_silence_timeout` | Raise it | +| Long IVR menus never reach a result | `timeout` | 30 s | Raise it, or hang up on `machine` without waiting for `READY` | +| Detection stops on the first thing it hears | `detect_message_end` | `false` in SWML, `true` in REST | Set it explicitly | +| Callers to your number wait in silence, then come back `unknown` | `initial_timeout` | 4.5 s | Lower it on inbound calls, and greet `unknown` as a person | + +A greeting from a business often runs past the defaults. This document gives it more room: + + + +```yaml +version: 1.0.0 +sections: + main: + - detect_machine: + detectors: amd + detect_message_end: true + initial_timeout: 6 + machine_voice_threshold: 2 + machine_ready_timeout: 2 + timeout: 60 + - switch: + variable: detect_result + case: + human: + - play: + url: "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow." + machine: + - play: + url: "say:This is Bayview Taxi. Your ride is confirmed for 8 AM tomorrow." + default: + - play: + url: "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow." + - hangup: {} +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { + "detect_machine": { + "detectors": "amd", + "detect_message_end": true, + "initial_timeout": 6, + "machine_voice_threshold": 2, + "machine_ready_timeout": 2, + "timeout": 60 + } + }, + { + "switch": { + "variable": "detect_result", + "case": { + "human": [ + { "play": {"url": "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."} } + ], + "machine": [ + { "play": {"url": "say:This is Bayview Taxi. Your ride is confirmed for 8 AM tomorrow."} } + ] + }, + "default": [ + { "play": {"url": "say:Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow."} } + ] + } + }, + { "hangup": {} } + ] + } +} +``` + + + +Test against the destinations you'll dial in production, not only your own phone. Carrier +voicemail, office phone systems, and mobile greetings each pause and speak differently. diff --git a/fern/products/platform/pages/calling/voice/overview.mdx b/fern/products/platform/pages/calling/voice/overview.mdx index 66eb307434..18d61bdc4b 100644 --- a/fern/products/platform/pages/calling/voice/overview.mdx +++ b/fern/products/platform/pages/calling/voice/overview.mdx @@ -21,6 +21,9 @@ Whether building a UCaaS solution, modernizing a legacy IVR, augmenting CX with The fundamentals of your first calling app + + Learn whether a person, voicemail, or fax machine is on a call you place or receive, and act on it + Get started with our Compatibility API @@ -135,7 +138,7 @@ The number of call legs is dependent on the call flow. A call is made up of mult -Yes, SignalWire will allow you to easily implement Answering Machine Detection (AMD) on your calls! AMD listens to the call to determine if the party that picked up is a real person or a voicemail machine. You can use this information to determine whether to leave a voicemail message or begin interacting with a real person. You can use AMD through [`detect_machine`](/docs/swml/reference/detect-machine) in SWML or `detect_answering_machine` using the [Realtime SDK](/docs/server-sdks/reference/python/relay/call). +Yes. Answering Machine Detection (AMD) listens after the destination answers and reports whether a person, a voicemail machine, or a fax machine picked up, so your call can leave a message, start an AI agent, or hang up. The [machine detection guide](/docs/platform/voice/machine-detection) walks through it with SWML, the REST Calling API, and the Server SDKs. For the details, see [`detect_machine`](/docs/swml/reference/calling/detect-machine) in SWML or [`detect_answering_machine`](/docs/server-sdks/reference/python/relay/call/detect-answering-machine) in the Server SDKs.