Reference
WebSocket API
The protocol every VM Hunter client speaks. Use it to build your own integration for a dialer or platform that is not covered by the bundled clients.
Endpoint
ws://app.vmhunter.com:2701One WebSocket connection per call. The server sends exactly one text frame, the verdict, and then closes the socket. Authentication is inside the first frame, not in an HTTP header.
ws:// on port 2701. Calls are dropped if no config frame arrives within 3 seconds of connecting.1. Config frame
The first frame must be JSON text with a config object:
{"config": {"vid": "lead-12345", "api_key": "{YOUR_API_KEY}", "sample_rate": 8000, "bytes_per_sample": 2}}| Field | Required | Meaning |
|---|---|---|
api_key | yes | Your API key. An unknown key is answered with FAILED / AUTH_FAILED and the socket closes. |
vid | yes | Your lead or call id. Shown in call logs and on the recording. Any string. |
sample_rate | no | Hz. Default 8000, which is what telephony delivers; send that. |
bytes_per_sample | no | Default 2 (16-bit). |
The config frame is where the call is checked against your plan. If the monthly allowance is used up the server replies FAILED with LIMIT_REACHED and the call is not counted.
2. Audio
After the config frame, send the callee's audio as binary frames: signed 16-bit little-endian PCM, mono, 8 kHz, with no headers. Chunk size is up to you; 20 ms (320 bytes) frames as they arrive from the call are ideal, since the engine classifies as the audio streams in.
Send only the far end. If your platform gives you a mixed or two-channel stream, pick the track that carries the callee, otherwise your own ringback or agent audio will be classified.
3. End of audio (optional)
{"eof": 1}Tells the engine you have no more audio, so it decides on what it has. You only need this if you stop streaming before the verdict arrives; the bundled clients send it after 3.5 seconds as a safety net. The engine also decides on its own if audio stops arriving.
4. Response
{"AMDSTATUS": "MACHINE", "AMDCAUSE": "MACHINE_BEEP"}Exactly one text frame, then the server closes the connection. Stop sending audio as soon as it arrives. Anything you send afterwards is discarded.
AMDSTATUS
| Value | Meaning | Suggested action |
|---|---|---|
HUMAN | A live person answered. | Connect to an agent. |
MACHINE | Voicemail, IVR, beep, static, silence or a disconnected number. | Drop or disposition using AMDCAUSE. |
CALLGUARD | Call screening. Sent only when your account is configured for it; otherwise screened calls come back as MACHINE with the CALLGUARD_PHRASE cause. | Treat as a person: a short intro, then an agent. |
FAILED | Detection did not run. Not billed. | Apply your fail-safe; the bundled clients default to MACHINE. |
AMDCAUSE
With HUMAN
| Value | Meaning |
|---|---|
HUMAN | Natural speech: a greeting followed by a pause, a question, or a live-person phrase. |
With MACHINE
| Value | Meaning |
|---|---|
MACHINE | A voicemail or IVR greeting phrase, or a greeting that ran on without pausing for the caller. |
MACHINE_BEEP | A voicemail beep was detected. |
MACHINE_STATIC | Only noise on the line. |
DISCONNECT | Special Information Tones: the number is disconnected or changed. Consider retiring the lead. |
INITIALSILENCE | No speech in the analysis window: a silent answer or no audio. |
MAXWORD | Too few words to classify and the speaker was cut off mid-phrase; defaulted to machine. |
CALLGUARD_PHRASE:<phrase> | Call screening was detected and the account replies MACHINE for it. The phrase that matched follows the colon. Branch on the CALLGUARD prefix. |
With CALLGUARD
| Value | Meaning |
|---|---|
CALLGUARD_PHRASE:<phrase> | The screening prompt that was recognised, e.g. an iPhone “Ask reason for calling” prompt or Google Call Screen. |
With FAILED
| Value | Meaning |
|---|---|
AUTH_FAILED | Unknown or inactive API key. |
LIMIT_REACHED | Monthly allowance used up. |
PERIOD_EXPIRED | The billing period has lapsed; renew in Billing. |
NO_SUBSCRIPTION | The account has no active plan. |
CPS_LIMIT | More calls started this second than the plan allows. Not billed; the call should go to an agent unscreened rather than be dropped. |
CHANNEL_LIMIT | More calls in detection at once than the plan allows. Not billed. |
CONFIG_TIMEOUT | No config frame within 3 seconds of connecting. |
SPEECHLLM_ENGINE_UNAVAILABLE | The engine could not process the call. The call is not billed. |
Timing
| Phase | Typical |
|---|---|
| Analysis window | The first 2 seconds of audio after the answer. |
| Silent calls | A call with no audible audio is answered right at the 2-second mark, without the wait for a final transcript. |
| Extension | If the callee is still mid-sentence at 2 seconds, the engine listens for up to 3 seconds before deciding. |
| Verdict | About 2.3 seconds after the answer on a normal call; the bundled clients cap streaming at 3.5 seconds. |
Play silence, not ringback or a prompt, while you wait. Anything you play to the callee during the window delays their greeting and the verdict with it.
Python example
A minimal client using websocket-client, streaming a raw PCM file as if it were a call:
import json, time
from websocket import create_connection
API_KEY = "{YOUR_API_KEY}"
ws = create_connection("ws://app.vmhunter.com:2701", timeout=10)
ws.send(json.dumps({"config": {"vid": "test-1", "api_key": API_KEY,
"sample_rate": 8000, "bytes_per_sample": 2}}))
with open("callee.raw", "rb") as f: # s16le mono 8 kHz, no header
while chunk := f.read(320): # 20 ms per frame
ws.send_binary(chunk)
time.sleep(0.02) # pace it like a live call
ws.send(json.dumps({"eof": 1}))
print(json.loads(ws.recv())) # {"AMDSTATUS": ..., "AMDCAUSE": ...}
ws.close()In a real integration, read the socket while you stream and stop as soon as the verdict arrives instead of sending the whole file. The Asterisk client shows one way to do that with select().