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

Endpoint
ws://app.vmhunter.com:2701

One 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.

Connections are plain 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:

Client → server (text)
{"config": {"vid": "lead-12345", "api_key": "{YOUR_API_KEY}", "sample_rate": 8000, "bytes_per_sample": 2}}
FieldRequiredMeaning
api_keyyesYour API key. An unknown key is answered with FAILED / AUTH_FAILED and the socket closes.
vidyesYour lead or call id. Shown in call logs and on the recording. Any string.
sample_ratenoHz. Default 8000, which is what telephony delivers; send that.
bytes_per_samplenoDefault 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.

Start streaming the moment the call is answered. The engine decides on roughly the first 2 seconds of audio, so any audio you buffer or delay beforehand pushes the verdict later on the call.

3. End of audio (optional)

Client → server (text)
{"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

Server → client (text)
{"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

ValueMeaningSuggested action
HUMANA live person answered.Connect to an agent.
MACHINEVoicemail, IVR, beep, static, silence or a disconnected number.Drop or disposition using AMDCAUSE.
CALLGUARDCall 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.
FAILEDDetection did not run. Not billed.Apply your fail-safe; the bundled clients default to MACHINE.

AMDCAUSE

With HUMAN

ValueMeaning
HUMANNatural speech: a greeting followed by a pause, a question, or a live-person phrase.

With MACHINE

ValueMeaning
MACHINEA voicemail or IVR greeting phrase, or a greeting that ran on without pausing for the caller.
MACHINE_BEEPA voicemail beep was detected.
MACHINE_STATICOnly noise on the line.
DISCONNECTSpecial Information Tones: the number is disconnected or changed. Consider retiring the lead.
INITIALSILENCENo speech in the analysis window: a silent answer or no audio.
MAXWORDToo 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

ValueMeaning
CALLGUARD_PHRASE:<phrase>The screening prompt that was recognised, e.g. an iPhone “Ask reason for calling” prompt or Google Call Screen.

With FAILED

ValueMeaning
AUTH_FAILEDUnknown or inactive API key.
LIMIT_REACHEDMonthly allowance used up.
PERIOD_EXPIREDThe billing period has lapsed; renew in Billing.
NO_SUBSCRIPTIONThe account has no active plan.
CPS_LIMITMore calls started this second than the plan allows. Not billed; the call should go to an agent unscreened rather than be dropped.
CHANNEL_LIMITMore calls in detection at once than the plan allows. Not billed.
CONFIG_TIMEOUTNo config frame within 3 seconds of connecting.
SPEECHLLM_ENGINE_UNAVAILABLEThe engine could not process the call. The call is not billed.

Timing

PhaseTypical
Analysis windowThe first 2 seconds of audio after the answer.
Silent callsA call with no audible audio is answered right at the 2-second mark, without the wait for a final transcript.
ExtensionIf the callee is still mid-sentence at 2 seconds, the engine listens for up to 3 seconds before deciding.
VerdictAbout 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:

vmhunter_min.py
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().