Integrations
VICIdial / Asterisk
An EAGI client that streams the callee's audio to VM Hunter and sets AMDSTATUS and AMDCAUSE on the channel. It drops straight into a VICIdial routing extension, and works on any Asterisk dialplan.
Prerequisites
| Requirement | Notes |
|---|---|
| VICIdial or Asterisk | Any Asterisk with EAGI support (VICIdial ships it). Root access to edit the dialplan. |
| Python 3 | With the websocket-client package. Python 3.6 or newer is fine. |
| API key | From Dashboard → API Keys. |
| Network | Outbound TCP to app.vmhunter.com:2701 from every dialer server. |
Install
Install Python and the WebSocket library
VICIdial runs on openSUSE, so the example uses zypper. Use apt or yum on other systems.
Terminalzypper in -y python3-pip pip3 install --upgrade pip pip3 install websocket-clientDownload the client
Unpack
vmhunter.agi.pyinto the Asterisk AGI directory. You can also download it from the dashboard.Terminalwget -N -O vmhunter.tar.gz https://app.vmhunter.com/vmhunter.tar.gz tar zxvf vmhunter.tar.gz --directory /var/lib/asterisk/agi-bin chmod +x /var/lib/asterisk/agi-bin/vmhunter.agi.pySet your API key
The script reads its settings from environment variables. Asterisk must see them, so put them where its process picks them up:
/etc/environmenton most installs, or the unit'sEnvironment=lines if Asterisk runs under systemd. Restart Asterisk after editing./etc/environmentAMD_WS_API_KEY="{YOUR_API_KEY}" AMD_LOG_FILE="/var/log/vmhunter.agi.log"To confirm Asterisk can see the variable:cat /proc/$(pidof asterisk)/environ | tr '\0' '\n' | grep AMD_Add the routing extension
Add this to the context your campaigns use (
/etc/asterisk/extensions.confon VICIdial). It runs VM Hunter, then hands the verdict to VICIdial's ownVD_amd.agi, which drops the call whenAMDSTATUS=MACHINE./etc/asterisk/extensions.confexten => 8370,1,AGI(agi://127.0.0.1:4577/call_log) exten => 8370,n,Playback(sip-silence) exten => 8370,n,EAGI(/var/lib/asterisk/agi-bin/vmhunter.agi.py,${VID}) exten => 8370,n,NoOp(VMHUNTER AMDSTATUS=${AMDSTATUS} RAW=${AMDSTATUS_RAW} CAUSE=${AMDCAUSE}) ; optional: give disconnected numbers their own disposition so the lead is not recycled exten => 8370,n,GotoIf($["${AMDCAUSE}"="DISCONNECT"]?disc) exten => 8370,n,AGI(VD_amd.agi,${EXTEN}) exten => 8370,n,AGI(agi-VDAD_ALL_outbound.agi,NORMAL-----LB-----${CONNECTEDLINE(name)}) exten => 8370,n,Hangup() exten => 8370,n(disc),AGI(agi-VDAD_ALL_outbound.agi,DISCONNECT-----LB-----${CONNECTEDLINE(name)}) exten => 8370,n,Hangup()Plain Asterisk without VICIdial: replace the two
AGI(...)lines with your own branch on${AMDSTATUS}, for exampleGotoIf($["${AMDSTATUS}"="HUMAN"]?agent:hangup).Reload and test
Terminalasterisk -rx "dialplan reload" tail -f /var/log/vmhunter.agi.logPlace a test call. Each call logs a line with the verdict, the cause and how long the round trip took. The same call appears in your call logs with the transcript and a recording of what the engine heard.
VICIdial campaign settings
Point each outbound campaign at the new extension. Nothing else in the campaign changes.
| Setting | Value |
|---|---|
| AMD Routing Extension | 8370 |
| AMD Type | AMD |
| AMD Method | EAGI |
Roll out one campaign first
Keep production campaigns on your current extension, move one campaign to 8370, and compare its answering-machine and dead-air rates for a day before moving the rest.Call screening (CALLGUARD)
iPhone's “Ask reason for calling”, Google Call Screen and similar features answer the call with a prompt and let a real person listen. VM Hunter labels those calls AMDCAUSE=CALLGUARD_PHRASE:<phrase>. By default the client maps them to AMDSTATUS=HUMAN so they reach an agent.
A better experience is to play a short recorded introduction first, so the screener hears who is calling, then connect the agent. The 8371 extension below does that; use it for one test campaign.
exten => 8371,1,AGI(agi://127.0.0.1:4577/call_log)
exten => 8371,n,Playback(sip-silence)
exten => 8371,n,EAGI(/var/lib/asterisk/agi-bin/vmhunter.agi.py,${VID})
exten => 8371,n,NoOp(VMHUNTER AMDSTATUS=${AMDSTATUS} RAW=${AMDSTATUS_RAW} CAUSE=${AMDCAUSE})
exten => 8371,n,GotoIf($["${AMDCAUSE:0:9}" = "CALLGUARD"]?callguard)
exten => 8371,n,AGI(VD_amd.agi,${EXTEN})
exten => 8371,n,AGI(agi-VDAD_ALL_outbound.agi,NORMAL-----LB-----${CONNECTEDLINE(name)})
exten => 8371,n,Hangup()
exten => 8371,n(callguard),Playback(vmhunter-callguard-intro)
exten => 8371,n,Set(AMDSTATUS=HUMAN)
exten => 8371,n,AGI(VD_amd.agi,${EXTEN})
exten => 8371,n,AGI(agi-VDAD_ALL_outbound.agi,NORMAL-----LB-----${CONNECTEDLINE(name)})
exten => 8371,n,Hangup()Record the intro as 8 kHz mono WAV at /var/lib/asterisk/sounds/vmhunter-callguard-intro.wav, three to five seconds, naming your company and the reason for the call:
sox intro.mp3 -r 8000 -c 1 -b 16 -e signed-integer /var/lib/asterisk/sounds/vmhunter-callguard-intro.wavPlayback fails and Asterisk hangs up the call. Check the TCPA and prerecorded-message rules that apply to your campaigns before playing recordings to screened calls.Configuration
All settings are environment variables. Only the API key is required.
| Variable | Default | Purpose |
|---|---|---|
AMD_WS_API_KEY | — | Your API key. Required. |
AMD_WS_URL | ws://app.vmhunter.com:2701 | Engine endpoint. |
AMD_AUDIO_SECONDS | 3.5 | Maximum audio streamed. The engine normally answers at about 2.3 s; the client stops as soon as the reply arrives. |
AMD_CALLGUARD_STATUS | HUMAN | How to map screened calls: HUMAN connects, MACHINE drops, CALLGUARD passes the raw status through for your own branch. |
AMD_FAILED_STATUS | MACHINE | Fail-safe status when the engine is unreachable or rejects the call. |
AMD_LOG_FILE | /var/log/vmhunter.agi.log | Per-call log. |
AMD_DEBUG | 0 | Set to 1 for verbose logging. |
Channel variables set on every call:
| Variable | Values |
|---|---|
AMDSTATUS | HUMAN or MACHINE. Route on this one; it is what VD_amd.agi reads. |
AMDSTATUS_RAW | The engine's untouched verdict: HUMAN, MACHINE, CALLGUARD or FAILED. |
AMDCAUSE | The reason, e.g. MACHINE_BEEP, DISCONNECT, INITIALSILENCE, CALLGUARD_PHRASE:…. Full list in the API reference. |
Troubleshooting
| Symptom | Check |
|---|---|
AMDCAUSE=AUTH_FAILED | Asterisk does not see AMD_WS_API_KEY, or the key was regenerated. Restart Asterisk after changing the environment. |
AMDCAUSE=LIMIT_REACHED | The monthly allowance is used up. Upgrade in Billing; the counter resets each period. PERIOD_EXPIRED and NO_SUBSCRIPTION mean the subscription needs attention in Billing too. |
AMDCAUSE=SPEECHLLM_ENGINE_UNAVAILABLE | The engine could not process the call. It is not billed; the client applies AMD_FAILED_STATUS. |
AMDCAUSE=CPS_LIMIT or CHANNEL_LIMIT | Your dialer is starting calls faster, or holding more at once, than your plan allows. Not billed. Lower the campaign's dial level or upgrade; set AMD_FAILED_STATUS=HUMAN so these calls reach an agent instead of being dropped. |
| Every call is MACHINE with INITIALSILENCE | No audio reaches the script. Make sure the extension uses EAGI, not AGI, and that the call is answered before the extension runs. |
| Connection errors in the log | Outbound TCP 2701 is blocked. Test with nc -zv app.vmhunter.com 2701. |