Docs / AMD_WS module

AMD_WS() — Native Asterisk Module

A compiled Asterisk dialplan application that streams the first seconds of an answered call to the AMDY.IO answering machine detection service and sets the same AMDSTATUS / AMDCAUSE variables that Asterisk's built-in AMD() and the AMDY EAGI client (amd.py) set. It replaces the Python EAGI script on any Asterisk 16, 18, 20+ system, ViciDial/Vicibox or a custom dialer, without Python and without touching your Asterisk build. Current version: v2.0.1.

Why the module over the EAGI script

  • No Python and no AGI process per call — the module talks WebSocket from inside Asterisk.
  • Uses Asterisk's own WebSocket client (res_http_websocket, part of every standard build), so nothing else is compiled from source.
  • Every wait is bounded by a real clock; hangups are detected immediately.
  • A prompt can be played to the callee while detection runs (built-in parallel playback).

Requirements

ItemRequirement
Asterisk16, 18, 20, 21 or 22, with the res_http_websocket module (part of every standard build). Asterisk 13 is not supported.
Operating systemAny Linux with gcc, make, pkg-config, tar and curl (the installer adds them). Tested on Vicibox/openSUSE, AlmaLinux/Rocky/CentOS, Debian/Ubuntu.
HeadersThe installer finds the headers of the running Asterisk (source tree in /usr/src, installed headers, or the distro -devel package). Asterisk is never recompiled or restarted.
NetworkOutbound TCP to api.amdy.io port 2700 from the telephony server.
OptionalMariaDB/MySQL client development files, only for the ViciDial phone/country lookup. Without them the module works with the lookup disabled.

Install (one command)

Log in as root on the telephony server, then:

curl -fsSL https://raw.githubusercontent.com/nikvb/amd/v2.0.1/install.sh | bash -s -- -y

The installer detects the running Asterisk and its headers, installs the build tools, compiles the module for exactly that Asterisk, checks the result, backs up any previous app_amd_ws.so, installs and loads the module without hanging up any call, and prints the dialplan snippet. Everything is logged to /var/log/app_amd_ws-install.log.

The v2.0.1 tag URL never changes. The sha256 of the released install.sh is in the release notes.

Install the detection prompt too

Add --playfile to also install AMDY's insert.wav (0.6 s, 8 kHz mono) as /var/lib/asterisk/sounds/amdy/insert.wav; the installer then prints the 8370 line with amdy/insert as the playback argument. --playfile ambiguous.wav installs the 2 s variant; --playfile /path/own.wav installs your own file (converted with sox if needed).

curl -fsSL https://raw.githubusercontent.com/nikvb/amd/v2.0.1/install.sh | bash -s -- -y --playfile

Preview, no database, uninstall

# see what would happen, change nothing
curl -fsSL https://raw.githubusercontent.com/nikvb/amd/v2.0.1/install.sh | bash -s -- --dry-run

# no MySQL dependency, no DB lookup
curl -fsSL https://raw.githubusercontent.com/nikvb/amd/v2.0.1/install.sh | bash -s -- -y --no-db

# remove the module again
curl -fsSL https://raw.githubusercontent.com/nikvb/amd/v2.0.1/install.sh | bash -s -- --uninstall

Verify the install

asterisk -rx 'module show like app_amd_ws'
asterisk -rx 'core show application AMD_WS'
asterisk -rx 'amd_ws show settings'

amd_ws show settings shows the effective configuration, DB availability, and per-outcome counters since the module was loaded.

Dialplan

AMD_WS([host[,port[,vid[,timeout_ms[,playfile[,options]]]]]])
ParameterDefaultMeaning
hostapi.amdy.io (from amd_ws.conf)AMD service host.
port2700TCP port.
vid${CALLERID(name)}Call tracking id (ViciDial keeps its call id in the caller id name).
timeout_ms10000Detection window, counted from the first audio frame.
playfilenoneSound to play to the callee while detection runs (Playback() semantics: no extension, & joins several files). Stops the instant a verdict arrives.
optionsnoneFlags, see Options below.

ViciDial / Vicibox (extension 8370)

Replace the EAGI(...amd.py) line of your existing 8370 block; everything else stays:

exten => 8370,1,AGI(agi://127.0.0.1:4577/call_log)
exten => 8370,n,Playback(sip-silence)
exten => 8370,n,AMD_WS(api.amdy.io,2700,${CALLERID(name)},10000)
exten => 8370,n,GotoIf($["${AMDCAUSE}" = "CONNECTION_ERROR" | "${AMDCAUSE}" = "PROCESSING_ERROR" | "${AMDCAUSE}" = "FATAL_ERROR"]?amd_fallback:continue)
exten => 8370,n(amd_fallback),AMD(2000,2000,1000,5000,120,50,4,256)
exten => 8370,n(continue),AGI(VD_amd.agi,${EXTEN})
exten => 8370,n,AGI(agi-VDAD_ALL_outbound.agi,NORMAL-----LB-----${CONNECTEDLINE(name)})

Then asterisk -rx 'dialplan reload'. Set the campaign's Routing Extension to 8370, AMD Agent Route Options to ENABLED, and the AMD_AGENT_OPT_<campaign> container entry to exactly HUMAN,HUMAN — the same settings as the EAGI install (see the AMD config guide). The phone number and country code are looked up in vicidial_auto_calls automatically, as the EAGI client did.

Generic Asterisk (custom dialer)

exten => s,1,Answer()
exten => s,n,AMD_WS(api.amdy.io,2700,${UNIQUEID},10000,,n)
exten => s,n,GotoIf($["${AMDSTATUS}" = "MACHINE"]?machine:human)
exten => s,n(human),Dial(...)                       ; live person
exten => s,n(machine),Hangup()                     ; or leave a message

Option n skips the ViciDial database lookup on a box that has none.

Play a prompt during detection

exten => 8370,n,AMD_WS(api.amdy.io,2700,${CALLERID(name)},10000,custom/hello,d(500))

Plays custom/hello (from /var/lib/asterisk/sounds) starting 500 ms in, while the callee's audio is analysed; playback stops as soon as the verdict is known. The file must be 8 kHz mono (sox in.wav -r 8000 -c 1 -b 16 hello.wav). Do not add a separate Playback() line for the prompt — it would play before detection starts.

Call screening (iPhone, Samsung, Google Voice)

When the service reports MACHINE with AMDCAUSE starting with CALLASSISTSCRNAMD, GVOICEAMD or SCREENINGAMD, a screening robot answered. The module exports AMDPHONE / AMDCOUNTRYCODE, so the dialplan can hold the screened leg and redial the number once: the second call arrives as call waiting, rings the person, lands on 8370 for a fresh detection and is routed to an agent as usual. The redial uses the exported AMDPHONE:

exten => 8370,n(screen_check),Set(AMDCLASS=${CUT(AMDCAUSE,-,1)})
exten => 8370,n,GotoIf($["${AMDSTATUS}" = "MACHINE" & "${SCREEN_REDIAL}" != "1" & "${AMDPHONE}" != "" & ("${AMDCLASS}" = "CALLASSISTSCRNAMD" | "${AMDCLASS}" = "GVOICEAMD" | "${AMDCLASS}" = "SCREENINGAMD")]?amdws-screen-hold,s,1)
exten => 8370,n(continue),AGI(VD_amd.agi,${EXTEN})
...
[amdws-screen-hold]
exten => s,1,Originate(Local/9${AMDPHONE}@default,exten,default,8370,1,55,c(${CALLERID(num)})n(${CALLERID(name)})v(SCREEN_REDIAL=1))
exten => s,n,GotoIf($["${ORIGINATE_STATUS}" = "SUCCESS"]?answered)
exten => s,n,Set(AMDSTATUS=MACHINE)
exten => s,n,Set(AMDCAUSE=${AMDCLASS}-REDIAL-${ORIGINATE_STATUS})
exten => s,n,Goto(default,8370,continue)
exten => s,n(answered),Hangup()
exten => h,1,NoOp(screened leg released)

The held leg waits up to 55 s while the redial rings and is released the moment the second call is answered; if nobody answers, it returns to the normal flow as MACHINE. SCREEN_REDIAL=1 guarantees a single redial per screening. Full block and notes: vicidial-call-screening.md.

Options

OptionMeaning
nSkip the ViciDial phone/country lookup for this call.
p(phone) / k(code)Send this phone number / country code explicitly instead of looking them up.
i(cid)Send this value as caller_id (default: the channel's ${CALLERID(num)}).
d(ms)Delay before playfile starts.
c(ms)Connect timeout (default 10000).
sUse TLS (wss://).
ADo not answer the channel (default: answer if needed).
vTrace: log one line per event for this call (see Troubleshooting).

Results — channel variables

VariableValues
AMDSTATUSHUMAN, MACHINE, NOTSURE, HANGUP
AMDCAUSEHUMAN; the service's reply on a machine (e.g. AMD-4.50-0.95, NUMBERSAMD-4.50-0.93, OTHERAMD-4.50-0.92); or an error/timeout cause below
AMDSTATS<elapsed_ms>-<audio_ms_sent>-<chunks>-<bytes> (ViciDial logs the first number as run_time)
AMDRESPONSEThe last raw text the service sent
AMDELAPSEDMilliseconds from the first audio frame to the result
AMDPHONE, AMDCOUNTRYCODEThe number / country code sent to the service (ViciDial lookup or p()/k()), when known — used to redial after a call-screening verdict
SituationAMDSTATUSAMDCAUSE
Live personHUMANHUMAN
Answering machine / voicemailMACHINEservice reply, e.g. AMD-4.50-0.95
Service unreachable (DNS, firewall, connect timeout)HUMANCONNECTION_ERROR
Connection dropped during detectionHUMANPROCESSING_ERROR
Internal failure on the Asterisk sideHUMANFATAL_ERROR
No verdict within timeout_msNOTSURESERVER_TIMEOUT
No audio received at all (RTP never arrived)NOTSURENOAUDIODATA-<ms>
Callee hung up during detectionHANGUPHANGUP
Audio stopped mid-call, service could not finaliseNOTSUREEOF_INCONCLUSIVE / EOF_ERROR

Errors default to HUMAN on purpose: a call is never lost because the AMD service was unreachable — it goes to an agent, or in ViciDial to the built-in AMD() via the fallback line above. NOAUDIODATA-<ms> and HANGUP are the exact values Asterisk's built-in AMD() uses, so ViciDial's dead-air handling (NOAUDIODATA-Hangup-ENABLED, disposition ADAIR) works unchanged.

Configuration file (optional)

/etc/asterisk/amd_ws.conf — copy from amd_ws.conf.sample; reload with asterisk -rx 'module reload app_amd_ws.so'. Nothing is required; the dialplan arguments override the file.

KeyDefaultMeaning
host, portapi.amdy.io, 2700Service endpoint when the dialplan gives none.
timeout_ms10000Detection window.
connect_timeout_ms10000Connect timeout; lower it (2000–3000) to fall back faster when the service is unreachable.
send_schedule500,1000,1500,2000,3000,…,9000When (ms from the first audio frame) audio is sent. A 0.5 s schedule delivers audio up to 0.5 s sooner.
tls, tls_verify, tls_cafileno, yes, system CAwss:// connections.
db, db_timeout_msyes, 1000ViciDial phone/country lookup (credentials from /etc/astguiclient.conf).
send_caller_idyesSend ${CALLERID(num)} as caller_id.
playdelay_ms0Default delay before playfile.
tracenoPer-call event timeline in the log for every call.
extra_configemptyExtra service options as JSON, e.g. {"short_no_greeting":true} (verdict ~0.5 s earlier, but without the machine type) or {"detection_mode":"aggressive"}.

How long does a verdict take?

Typically 5–6 seconds after the callee's first audio. The service classifies speech in stages and gives its final answer (including which kind of machine) once it has 4.5 s of speech; leading silence does not count. Audio is sent on the schedule above and the service answers within about 50 ms of the chunk that completes its analysis. A live person is usually recognised earlier.

+60ms    connected, config sent
+1501ms  first audio frame                  <- detection window starts
+2002ms  chunk #1 (0.5 s of audio) -> ack 57 ms
+3501ms  chunk #4 (2.0 s)          -> ack 26 ms
+5501ms  chunk #6 (4.0 s)          -> ack 49 ms
+6501ms  chunk #7 (5.0 s)          -> AMD-4.50-0.9491 after 50 ms
status=MACHINE cause=AMD-4.50-0.9491 elapsed=5049 sent=80320 chunks=7

To trade the machine type for speed set extra_config={"short_no_greeting":true}; to remove the schedule granularity use the 0.5 s send_schedule.

Troubleshooting

Health check in one line:

asterisk -rx 'module show like app_amd_ws'; asterisk -rx 'amd_ws show settings' | head -30; timeout 5 bash -c 'exec 3<>/dev/tcp/api.amdy.io/2700' && echo "port 2700 open"

Every call writes two lines to /var/log/asterisk/messages (or full); every line carries vid=, so grep 'vid=V923…' returns one whole call:

AMD_WS: SIP/trunk-0004706c vid=V9231813370204367076 host=api.amdy.io:2700 play=none
AMD_WS: SIP/trunk-0004706c vid=V9231813370204367076 status=MACHINE cause=OTHERAMD-4.50-0.9280 elapsed=6106 sent=96320 chunks=8
AMDCAUSEWhat it meansWhat to check
CONNECTION_ERRORThe server could not reach the serviceOutbound TCP 2700 to all api.amdy.io addresses (dig +short api.amdy.io); DNS; module show like res_http_websocket
PROCESSING_ERRORConnected, then the connection brokeMiddleboxes cutting WebSockets; ${AMDRESPONSE}; contact support with the VID
SERVER_TIMEOUTConnected, audio sent, no verdict in time${AMDRESPONSE}; contact support with the VID
NOAUDIODATA-<ms>No audio ever reached AsteriskRTP/NAT/codec on the trunk; rtp set debug on — not an AMD problem
FATAL_ERRORModule-internal (codec, memory, config)AMD_WS warnings in the log; amd_ws.conf
empty variablesAMD_WS() never randialplan show 8370@default; module loaded?

For a full timeline of one call add option v (or trace=yes) and grep AMD_WS: — you see the connect, the first audio frame, every chunk sent, every reply and the result with millisecond offsets. An empty reply ("") in the trace is the service acknowledging a chunk, not a failure; keep reading for the final HUMAN or machine reply.

Upgrading / uninstalling

Re-run the install command: it builds the new version, backs up the old module (app_amd_ws.so.bak.<timestamp>) and swaps it in without dropping calls (if the module is busy it waits, never hangs up). --uninstall removes the module; your dialplan is never modified by the installer. Upgrading from 1.x: the error causes changed from NETERR / INTERR to CONNECTION_ERROR / PROCESSING_ERROR / FATAL_ERROR, so update the fallback line in your 8370 block (shown above).

Full reference

Source, changelog, installer flags, wire protocol, compatibility matrix and the full troubleshooting playbook: github.com/nikvb/amd (v2.0.1 release).

Support

Need help? Email [email protected] with:

  • Asterisk version (asterisk -V)
  • Installer log (/var/log/app_amd_ws-install.log)
  • The two AMD_WS: log lines for one call, or its trace (option v)
AMD_WS() — native Asterisk module for AMDY answering machine detection | AMDY.IO