Skip to navigation

Detect machines

View as MarkdownOpen in Claude

Machine detection lets SignalWire identify whether a person, voicemail, or a fax machine is on the call so your app can choose what happens next. This guide shows two flows: greet a person or leave a voicemail, and send a document when a fax machine answers.

How detection works

SignalWire listens to audio on an answered call. A short greeting followed by silence suggests a person; longer speech suggests voicemail or an IVR menu. Fax detection listens for a fax tone. If there isn’t enough audio to decide, your app needs a fallback for the uncertain result.

Voicemail detection can also wait for the greeting to end before you leave a message. The sections below explain how to handle these results in each approach.

Choose an approach

Follow the section for your app’s approach:

  • SWML: add detect_machine to your call document and branch on the result. Optionally send events to a webhook.
  • WebSocket (Relay): start detection on an answered call and handle events in your code. No public URL is needed.
  • REST: start detection by call ID from any process and receive results at a webhook.

Prepare for detection

Start with working call setup from Make and receive calls. Test AMD (answering machine detection) with a phone you can answer or send to voicemail. To test fax detection, use a fax destination and a publicly accessible PDF.

Trial projects only call and receive calls from verified numbers

A trial project only places calls to and receives calls from numbers you’ve purchased or verified. Prepare for your first call shows how to verify the phone you’ll test with.

Messages left by artificial voices are regulated

Prerecorded and synthesized voicemail messages fall under consent, do-not-call, and calling-hour rules. Read the TCPA guide before you dial anyone but yourself.

Replace these values in the samples you run:

ValueReplace with
<YOUR_SPACE>Your Space’s subdomain in <YOUR_SPACE>.signalwire.com
<YOUR_PROJECT_ID>Your Project ID
<YOUR_API_TOKEN>Your API token, used only in server code
<YOUR_CALLER_ID>Your SignalWire phone number, or a verified caller ID, in E.164 format
<YOUR_DESTINATION>The phone or fax number you’ll call, in E.164 format
<YOUR_FAX_DOCUMENT_URL>The public URL of a PDF that SignalWire can fetch and fax

Detect machines via SWML

detect_machine pauses the document by default, then sets detect_result. Use switch to choose the next action.

detect_resultMeaningTypical action
humanA short greeting followed by silencePlay a message or connect an agent
machineSpeech exceeds the voice or word threshold, or a beep is heardLeave voicemail if you waited for the greeting to end; otherwise hang up
faxThe configured fax tone was detectedSend or receive a fax
unknownNo decision before timeoutGreet the caller or use another fallback
failedInvalid settings, missing status_url with wait: false, or detection already runningLog the failure and use a fallback
detectingBackground detection is running with wait: falseHandle events at status_url

detect_machine_beep is true when the machine detector heard a beep; otherwise it is false. With detectors: amd,fax, a fax tone can register as a beep, so a fax result can also report true. detect_ms holds the elapsed milliseconds, or -1 with wait: false.

Detect voicemail via SWML

Use AMD to distinguish a person from voicemail, then branch on the result. detect_message_end: true waits for a beep or a pause of machine_ready_timeout before the machine branch plays. The default branch also greets unknown and failed results.

The SDK programs place the call as in Place a call via SWML. Each branch is built with the SDK’s SWML builder. The YAML and JSON tabs contain the equivalent complete document.

# 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="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
host="<YOUR_SPACE>.signalwire.com",
)
live = (
SWMLBuilder(SWMLService(name="live-message"))
.say("Hello! Your Bayview Taxi ride is confirmed for 8 AM tomorrow.")
.build()["sections"]["main"]
)
voicemail = (
SWMLBuilder(SWMLService(name="voicemail-message"))
.say(
"This is Bayview Taxi. Your ride is confirmed for 8 AM tomorrow. "
"Reply to our text message if you need to change it."
)
.build()["sections"]["main"]
)
swml = (
SWMLBuilder(SWMLService(name="leave-a-message"))
.detect_machine(detectors="amd", detect_message_end=True, timeout=45)
.switch(
variable="detect_result",
case={
"human": live,
"machine": voicemail,
},
default=live,
)
.hangup()
.build()
)
call = client.calling.dial(
from_="<YOUR_CALLER_ID>",
to="<YOUR_DESTINATION>",
swml=swml,
)
print(call["id"])

Run the example and answer with “Hello?” to hear the live confirmation. Run it again and send the call to voicemail to test the message after the greeting. The 45-second timeout allows time for the greeting; tune the silence threshold below if playback starts before the beep.

To reject voicemail instead, set detect_message_end: false and use hangup in the machine branch. An AI handoff can use ai in the human branch.

Detect a fax machine via SWML

Detect whether a fax machine or a person answers with detectors: "amd,fax". The fax branch sends a PDF, the human branch plays a spoken message, and the default branch hangs up.

CED is the default tone, sent by an answering fax machine. A fax tone can resemble a voicemail beep, so if amd decides first, SignalWire briefly waits for the fax detector. A detected fax tone takes priority.

# 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="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
host="<YOUR_SPACE>.signalwire.com",
)
send_receipt = (
SWMLBuilder(SWMLService(name="send-receipt"))
.send_fax(document="<YOUR_FAX_DOCUMENT_URL>", header_info="Bayview Taxi receipt")
.build()["sections"]["main"]
)
not_a_fax = (
SWMLBuilder(SWMLService(name="not-a-fax"))
.say("Hello, this is Bayview Taxi. We tried to fax your receipt. We will email it instead.")
.build()["sections"]["main"]
)
hang_up = (
SWMLBuilder(SWMLService(name="hang-up"))
.hangup()
.build()["sections"]["main"]
)
swml = (
SWMLBuilder(SWMLService(name="detect-fax"))
.detect_machine(detectors="amd,fax", timeout=30)
.switch(
variable="detect_result",
case={
"fax": send_receipt,
"human": not_a_fax,
},
default=hang_up,
)
.hangup()
.build()
)
call = client.calling.dial(
from_="<YOUR_CALLER_ID>",
to="<YOUR_DESTINATION>",
swml=swml,
)
print(call["id"])

Test with a fax number to confirm document delivery, then with a phone to hear the spoken fallback. For a fax machine calling you, use tone: "CNG" and receive_fax instead; its status_url receives the document details. Inbound call setup is covered in Answer a call via SWML.

Track detection results via SWML

Add status_url to detect_machine to receive JSON webhooks. These report intermediate detector events; the document still branches on detect_result.

Webhook fieldValue
event_typecalling.call.detect
params.call_idThe call ID
params.control_idThe detection operation’s ID
params.detect.typemachine or fax
params.detect.params.eventThe detector event listed below
params.detect.params.beeptrue once a beep has been heard; absent from fax events
OutcomeCallbacks, in order
A person answersHUMAN, finished
A machine answers, detect_message_end offMACHINE, finished, with READY between them if the line is already silent
A machine answers, detect_message_end onMACHINE, READY when the greeting ends, finished
A beep is heard before MACHINEMACHINE, then READY, then finished, regardless of detect_message_end
No decisionUNKNOWN at initial_timeout, then finished at timeout
A fax tone is heardCED or CNG, then finished

UNKNOWN doesn’t stop detection or resume the document. A silent call waits until timeout before detect_result becomes unknown. If the call hangs up during machine detection, no final finished callback is sent; fax detection still sends finished. Once set, beep: true remains on later machine events.

For background detection, set wait: false and supply status_url. The document continues immediately with detect_result: detecting; act on the webhook events instead. See the webhooks guide for endpoint setup and testing.

Tune SWML detection

Set these properties on detect_machine. Change one at a time and test with the destinations you expect to call.

SettingDefaultWhen to change it
initial_timeout4.5 sRaise it for greetings that start with silence
machine_voice_threshold1.25 sRaise it if longer human greetings are classified as machines
machine_words_threshold6 wordsLower it if short recorded greetings are classified as people
end_silence_timeout1.0 sLower it if a person’s pause after “Hello?” isn’t recognized
machine_ready_timeoutSame as end_silence_timeoutRaise it for voicemail greetings that pause before the beep
timeout30 sRaise it for long greetings; lower it for inbound callers waiting silently
detect_message_endfalseSet to true to wait through a voicemail greeting

For inbound screening, greet unknown callers and keep timeout short: they may be waiting for you to speak. detect_interruptions is unsupported in SWML and makes detection fail. The detect_machine reference lists all properties, including the detectors selector, which defaults to both amd and fax.

Detect machines via WebSocket (Relay)

Start detection on an answered call with detect() and a machine or fax detector. Each operation runs one detector, and a call can run only one detector of each type at a time.

Register a calling.call.detect handler before starting detection. Read event.detect.type for the detector and event.detect.params.event for its result:

EventMeaningTypical action
HUMANA short greeting followed by silencePlay a message or connect an agent
MACHINEA recorded greeting or beep was detectedWait for READY to leave voicemail, or hang up
READYA beep or the end of the greetingPlay the voicemail message
NOT_READYSpeech resumed after READY, with detect_interruptions enabledWait for the next READY
UNKNOWNNothing heard before initial_timeoutGreet the caller or keep waiting
CED or CNGThe configured fax toneSend or receive a fax
finishedDetection endedClean up if no action has been taken

An action’s wait() returns on the first result. To leave voicemail, follow events through MACHINE to READY instead. detect_message_end defaults to true; setting it to false stops on MACHINE. A beep heard before MACHINE triggers MACHINE and then READY with either setting; with false, detection has already stopped by the time a later beep plays, so it’s usually missed.

UNKNOWN doesn’t stop the detector: it keeps listening until it decides or timeout expires. finished follows the final result, including when the call hangs up during detection. Once a beep is heard, event.detect.params.beep is true on that and later machine events; fax events don’t carry it. Call the action’s stop() to stop detection early.

Detect voicemail via WebSocket (Relay)

Use AMD events to distinguish a person from voicemail and detect when the greeting ends. The handler plays a live confirmation on HUMAN or UNKNOWN, leaves voicemail on READY, and hangs up if detection finishes without a usable result. The announced flag prevents later events from starting a second message.

Call setup follows Place a call via WebSocket (Relay).

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as leave_message_relay.py and run: python leave_message_relay.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="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
contexts=["default"],
)
async def main():
async with client:
call = await client.dial(
devices=[[{
"type": "phone",
"params": {
"from_number": "<YOUR_CALLER_ID>",
"to_number": "<YOUR_DESTINATION>",
"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(
{"type": "machine", "params": {"detect_message_end": True}}, timeout=45
)
await call.wait_for_ended()
asyncio.run(main())

Test once by answering with “Hello?” and once by sending the call to voicemail. To observe the sequence, log event.detect in the handler. To reject machines, hang up on MACHINE instead; to hand a person to an AI agent, call call.ai() on HUMAN.

Detect a fax machine via WebSocket (Relay)

Detect a fax machine by listening for CED, the default tone from an answering fax machine. On detection, send the PDF; otherwise, play a spoken fallback. Unlike AMD, this detector only decides whether it heard the fax tone; the fallback isn’t a human classification.

Listen for calling.call.fax with fax.type: finished to read fax.params.success and fax.params.pages. These SDK versions don’t complete fax.wait() for that payload, so the example handles the event directly.

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as detect_fax_relay.py and run: python detect_fax_relay.py
import asyncio
from signalwire.relay import RelayClient
from signalwire.relay.event import FaxEvent
NOT_A_FAX = "Hello, this is Bayview Taxi. We tried to fax your receipt. We will email it instead."
client = RelayClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
contexts=["default"],
)
def is_fax(event) -> bool:
detect = event.params.get("detect", {})
return detect.get("params", {}).get("event") in ("CED", "CNG")
async def main():
async with client:
call = await client.dial(
devices=[[{
"type": "phone",
"params": {
"from_number": "<YOUR_CALLER_ID>",
"to_number": "<YOUR_DESTINATION>",
"timeout": 30,
},
}]],
)
action = await call.detect({"type": "fax", "params": {"tone": "CED"}}, timeout=8)
event = await action.wait()
if is_fax(event):
async def on_fax(fax_event: FaxEvent):
if fax_event.fax.get("type") != "finished":
return
result = fax_event.fax.get("params", {})
print(f"Fax sent: {result.get('success')}, {result.get('pages', 0)} pages")
if call.state != "ended":
await call.hangup()
call.on("calling.call.fax", on_fax)
await call.send_fax(
document="<YOUR_FAX_DOCUMENT_URL>",
header_info="Bayview Taxi receipt",
)
await call.wait_for_ended()
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())

Test with a fax destination, then with a phone to check the fallback. For incoming faxes, listen for CNG and use receive_fax() in Python or receiveFax() in TypeScript. The finished fax event also includes the stored document URL in fax.params.document. For inbound call setup, see Answer a call via WebSocket (Relay).

Tune Relay detection

Pass timeout separately to detect(); put the other settings in the detector’s params.

SettingDefaultWhen to change it
initial_timeout4.5 sRaise it for delayed greetings; lower it to greet silent inbound callers sooner
machine_voice_threshold1.25 sRaise it if longer human greetings are classified as machines
machine_words_threshold6 wordsLower it if short recorded greetings are classified as people
end_silence_timeout1.0 sLower it if a person’s pause after “Hello?” isn’t recognized
machine_ready_timeoutSame as end_silence_timeoutRaise it for voicemail greetings that pause before the beep
timeout30 sRaise it for long greetings; keep fax checks short for voice callers
detect_message_endtrueSet explicitly; use false when you only need the initial classification
detect_interruptionsfalseEnable it to receive NOT_READY if speech resumes after READY

Change one setting at a time. For inbound screening, greet UNKNOWN callers: they may be waiting silently for you to speak. If you enable interruptions, handle NOT_READY and wait for the next READY before resuming your message.

Detect on a call already in progress via REST

If another process needs to start detection, send calling.detect with the answered call’s ID, a required control_id, a detect object, and status_url. Use detect.type: machine for AMD or detect.type: fax for fax tones; pass settings in detect.params and timeout separately. detect_message_end defaults to true, and fax tone defaults to CED (use CNG for incoming faxes).

POST
/api/calling/calls
curl -X POST https://{your_space_name}.signalwire.com/api/calling/calls \
-H "Content-Type: application/json" \
-u "<project_id>:<api_token>" \
-d '{
"command": "calling.detect",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"params": {
"control_id": "detect-control-1",
"detect": {
"params": {
"end_silence_timeout": 1,
"initial_timeout": 4.5
},
"type": "machine"
},
"timeout": 30,
"status_url": "https://example.com/webhooks/detection"
}
}'

The command returns immediately. Your webhook receives calling.call.detect events with the result in params.detect.params.event. Have the handler send the next call command, such as calling.play on READY for voicemail. Use calling.detect.stop with the same control_id to stop early. Only one detector of each type can run on a call at a time.