Docs / AMD_WS 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.
res_http_websocket, part of every standard build), so nothing else is compiled from source.| Item | Requirement |
|---|---|
| Asterisk | 16, 18, 20, 21 or 22, with the res_http_websocket module (part of every standard build). Asterisk 13 is not supported. |
| Operating system | Any Linux with gcc, make, pkg-config, tar and curl (the installer adds them). Tested on Vicibox/openSUSE, AlmaLinux/Rocky/CentOS, Debian/Ubuntu. |
| Headers | The 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. |
| Network | Outbound TCP to api.amdy.io port 2700 from the telephony server. |
| Optional | MariaDB/MySQL client development files, only for the ViciDial phone/country lookup. Without them the module works with the lookup disabled. |
Log in as root on the telephony server, then:
curl -fsSL https://raw.githubusercontent.com/nikvb/amd/v2.0.1/install.sh | bash -s -- -yThe 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.
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# 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 -- --uninstallasterisk -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.
AMD_WS([host[,port[,vid[,timeout_ms[,playfile[,options]]]]]])| Parameter | Default | Meaning |
|---|---|---|
| host | api.amdy.io (from amd_ws.conf) | AMD service host. |
| port | 2700 | TCP port. |
| vid | ${CALLERID(name)} | Call tracking id (ViciDial keeps its call id in the caller id name). |
| timeout_ms | 10000 | Detection window, counted from the first audio frame. |
| playfile | none | Sound to play to the callee while detection runs (Playback() semantics: no extension, & joins several files). Stops the instant a verdict arrives. |
| options | none | Flags, see Options below. |
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.
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 messageOption n skips the ViciDial database lookup on a box that has none.
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.
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.
| Option | Meaning |
|---|---|
| n | Skip 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). |
| s | Use TLS (wss://). |
| A | Do not answer the channel (default: answer if needed). |
| v | Trace: log one line per event for this call (see Troubleshooting). |
| Variable | Values |
|---|---|
| AMDSTATUS | HUMAN, MACHINE, NOTSURE, HANGUP |
| AMDCAUSE | HUMAN; 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) |
| AMDRESPONSE | The last raw text the service sent |
| AMDELAPSED | Milliseconds from the first audio frame to the result |
| AMDPHONE, AMDCOUNTRYCODE | The number / country code sent to the service (ViciDial lookup or p()/k()), when known — used to redial after a call-screening verdict |
| Situation | AMDSTATUS | AMDCAUSE |
|---|---|---|
| Live person | HUMAN | HUMAN |
| Answering machine / voicemail | MACHINE | service reply, e.g. AMD-4.50-0.95 |
| Service unreachable (DNS, firewall, connect timeout) | HUMAN | CONNECTION_ERROR |
| Connection dropped during detection | HUMAN | PROCESSING_ERROR |
| Internal failure on the Asterisk side | HUMAN | FATAL_ERROR |
No verdict within timeout_ms | NOTSURE | SERVER_TIMEOUT |
| No audio received at all (RTP never arrived) | NOTSURE | NOAUDIODATA-<ms> |
| Callee hung up during detection | HANGUP | HANGUP |
| Audio stopped mid-call, service could not finalise | NOTSURE | EOF_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.
/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.
| Key | Default | Meaning |
|---|---|---|
| host, port | api.amdy.io, 2700 | Service endpoint when the dialplan gives none. |
| timeout_ms | 10000 | Detection window. |
| connect_timeout_ms | 10000 | Connect timeout; lower it (2000–3000) to fall back faster when the service is unreachable. |
| send_schedule | 500,1000,1500,2000,3000,…,9000 | When (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_cafile | no, yes, system CA | wss:// connections. |
| db, db_timeout_ms | yes, 1000 | ViciDial phone/country lookup (credentials from /etc/astguiclient.conf). |
| send_caller_id | yes | Send ${CALLERID(num)} as caller_id. |
| playdelay_ms | 0 | Default delay before playfile. |
| trace | no | Per-call event timeline in the log for every call. |
| extra_config | empty | Extra service options as JSON, e.g. {"short_no_greeting":true} (verdict ~0.5 s earlier, but without the machine type) or {"detection_mode":"aggressive"}. |
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=7To 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.
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| AMDCAUSE | What it means | What to check |
|---|---|---|
| CONNECTION_ERROR | The server could not reach the service | Outbound TCP 2700 to all api.amdy.io addresses (dig +short api.amdy.io); DNS; module show like res_http_websocket |
| PROCESSING_ERROR | Connected, then the connection broke | Middleboxes cutting WebSockets; ${AMDRESPONSE}; contact support with the VID |
| SERVER_TIMEOUT | Connected, audio sent, no verdict in time | ${AMDRESPONSE}; contact support with the VID |
| NOAUDIODATA-<ms> | No audio ever reached Asterisk | RTP/NAT/codec on the trunk; rtp set debug on — not an AMD problem |
| FATAL_ERROR | Module-internal (codec, memory, config) | AMD_WS warnings in the log; amd_ws.conf |
| empty variables | AMD_WS() never ran | dialplan 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.
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).
Source, changelog, installer flags, wire protocol, compatibility matrix and the full troubleshooting playbook: github.com/nikvb/amd (v2.0.1 release).
Need help? Email [email protected] with:
asterisk -V)/var/log/app_amd_ws-install.log)AMD_WS: log lines for one call, or its trace (option v)