FAXAMD, CALLASSISTAMD, CALLASSISTSCRNAMD, CALLRECAMD, FAS2AMD, HELLOFASAMD. See Section 8.1.0/0.0) and strings "yes"/"no"/"on"/"off" (case-insensitive). See Section 9.STAGE-<stage>-<CLS>-<duration>-<confidence>. Multiple stages per chunk are semicolon-joined. See Section 9.stage_results flag introduced — streams STAGE-… progress frames between checkpoints. immediate_detection relaxed to accept true, 1, "true".
portal.amdy.io/client-api/ips) is retired. IP registration is now POST app.amdy.io/api/v1/ips/register — see Section 3.
The service delivers real-time, streaming answering-machine detection for telephony. You stream 8 kHz audio over WebSocket and receive a detection result as soon as the model reaches a confident decision.
ws://api.amdy.io:2700 with your API key in the header.| Item | Detail |
|---|---|
| API key | 64-character hex string — obtained from the portal at app.amdy.io |
| Public IP | Your server's outbound IP, registered via the Registration API |
| WebSocket client | Any standard WS library (Python websockets, Node ws, etc.) |
| Audio format | 8 kHz · 16-bit · mono · raw PCM (little-endian) |
API keys are 64-character hex strings. Retrieve yours from the Settings page on the portal.
Use either the Authorization header or the X-API-Key header:
Authorization: Bearer YOUR_API_KEY
# or
X-API-Key: YOUR_API_KEY
headers: { "X-API-Key": "YOUR_API_KEY" }
Every server that will stream audio to the detection API must have its public IP registered first. Registration is a one-time step per server — the install scripts handle it automatically. For manual integrations, call this endpoint once during setup.
https://app.amdy.io
Register a public IP for your account and activate the account if it is pending. Safe to call repeatedly — duplicate IPs return success.
Authorization: Bearer <api_key> or X-API-Key: <api_key>
X-Forwarded-For."Installed via installamd-v2.sh".curl -X POST https://app.amdy.io/api/v1/ips/register \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ip": "203.0.113.42", "description": "prod-dialer-01"}'
import requests
resp = requests.post(
"https://app.amdy.io/api/v1/ips/register",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"ip": "203.0.113.42", "description": "prod-dialer-01"},
)
resp.raise_for_status()
print(resp.json()) # {"ok": true, "activated": true, "ip": "203.0.113.42", ...}
# Omit "ip" — the server reads X-Forwarded-For automatically
curl -X POST https://app.amdy.io/api/v1/ips/register \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "auto-registered"}'
{
"ok": true,
"activated": true,
"account_activated_now": false,
"client_id": 1042,
"ip": "203.0.113.42",
"registered": { "ip": "203.0.113.42", "status": "registered" }
}
registered.status is "registered" or "already_registered".
400Missing or invalid IP address.
401Missing or invalid API key.
403API key exists but is not active.
502Internal registration failure. The response body includes a hint field — include it in any support request.
To view, rename, or remove registered IPs, use the Settings page in the portal. There is no public DELETE endpoint — removals are portal-only.
| Property | Value |
|---|---|
| Endpoint | ws://api.amdy.io:2700 |
| Transport | Plain WebSocket routed through HAProxy |
| Auth | X-API-Key header on connect |
| Gate | Connection is accepted only from registered IPs |
import websockets
headers = {"X-API-Key": "YOUR_API_KEY"}
ws = await websockets.connect("ws://api.amdy.io:2700", extra_headers=headers)
const WebSocket = require('ws');
const ws = new WebSocket('ws://api.amdy.io:2700', {
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
{
"config": {
"sample_rate": 8000,
"VID": "unique_call_id_12345",
"immediate_detection": false,
"stage_results": false
}
}
| Field | Required | Description |
|---|---|---|
sample_rate | Yes | Must be 8000. |
VID | Recommended | Unique call identifier — used in logs and the analytics portal. |
phone | No | Destination phone number. |
country_code / cc | No | 2-letter country code. |
caller_id / callerid | No | Caller ID string. |
max_detection_time | No | Seconds before forced result (0.5–10, default 8). |
immediate_detection | No | Enable fast mode. Truthy values: JSON boolean true, integer 1, float 1.0, or strings "true"/"yes"/"on"/"1" (case-insensitive). Falsy: false, 0, 0.0, "false"/"no"/"off", or absent. Default false. See Section 9. |
stage_results | No | Stream intermediate cascade progress frames (STAGE-…) instead of empty replies during detection. Accepts the same truthy/falsy values as immediate_detection. Default false. See Section 9. |
"" (empty string) — keep streaming (standard mode).STAGE-… — intermediate progress frame; keep streaming (stage_results mode, see Section 9).{"eof": 1} (text) or an empty binary frame. The server returns a forced result immediately.| Samples buffered | Model stage | Action |
|---|---|---|
| < 1 000 | — | No reply yet, keep streaming |
| 1 000 – 1 999 | 1k | Reply after this chunk |
| 2 000 – 2 999 | 2k | Reply after this chunk |
| 3 000 – 3 999 | 3k | Reply after this chunk |
| 4 000 – 7 999 | 4k | Reply after this chunk |
| 8 000 – 11 999 | 8k | Reply after this chunk |
| 12 000 – 15 999 | 12k | Reply after this chunk |
| 16 000 – 23 999 | 16k | Reply after this chunk |
| 24 000 – 31 999 | 24k | Reply after this chunk |
| ≥ 32 000 (~4.0 s) | 32k + greeting | Forced result |
CLASSIFICATION-DURATION_SECONDS-CONFIDENCE| Example | Meaning |
|---|---|
"" | Still detecting — keep streaming |
HUMAN-4.50-0.9950 | Live person, detected at 4.5 s, confidence 99.5% |
AMD-8.20-0.9234 | Generic answering machine |
WELCOMEVMAMD-4.50-1.0000 | Standard voicemail greeting |
HONEYPOTAMD-4.50-0.9876 | Anti-spam honeypot line |
VERIZONAMD-4.50-1.0000 | Carrier forwarding message |
SILENCEAMD-8.34-0.0000 | Insufficient speech after silence removal — treated as machine |
cls, dur, conf = result.split("-") # never more than 3 parts — classification never contains dashes
is_human = cls == "HUMAN"
duration = float(dur)
confidence = float(conf)
AMD. The greeting sub-classifier needs ~4.5 s of speech. If a call has a long silent lead-in, max_detection_time (default 8 s, clamp 0.5–10) can expire before enough speech accumulates. In that case the server returns the verdict of the deepest completed cascade stage — e.g. AMD-8.10-0.9419: 8.1 s streamed, ~3.9 s silence, all stages agreed machine, greeting stage never reached. The verdict is fully valid; only the machine subtype is absent. If subtypes matter more than latency, raise max_detection_time (up to 10).
PLVMSFASAMD is now PLVMSAMD. Any client code that hard-matches the old string will silently stop recognizing “please leave your message” greetings and fail to play voicemail drops for this pattern. Update your switch/case or if-chains before upgrading to a v4.2+ build.
HUMANAMDBEEPAMDBUSYAMDCANTLEAVEAMDCANTTAKEAMDCLICKAMDCUSTOMVMAMDDISCONNECTEDAMDVOICEMAILAMDGVOICEAMDGOOGLESUBAMDHELLOMSGAMDHONEYPOTAMDIVRAMDLADYBOTAMDMAILBOXFULLAMDMAGICJACKAMDMUSICAMDNETWORKAMDPLVMSAMDPLVMSFORAMDSPANISHAMDRINGINGAMDNUMBERSAMDSCREENINGAMDVMNOTSETUPAMDSUBNOTAVAILAMDTELAMDTHANKYOUAMDWELCOMEVMAMDFASAMDVOICEMAILV2AMDFASV3AMDFASV4AMDFAS2AMDHELLOFASAMDFAXAMDCALLASSISTAMDCALLASSISTSCRNAMDCALLRECAMDVERIZONAMDSILENCEAMDSILENCEHUMAN.
Streams through the full cascade (1k → 2k → 3k → 4k → 8k → 12k → 16k → 24k → 32k + greeting at 4.5 s of speech). Returns "" after each chunk until a high-confidence verdict. Accuracy ~99%, typical latency 3–4 seconds.
{"config": {"sample_rate": 8000, "VID": "...", "immediate_detection": false}}
| Chunk | Buffer | Reply |
|---|---|---|
| 1 | 4k samples | "" — keep going |
| 2 | 8k samples | "" — keep going |
| 3 | 12k samples | "" — keep going |
| 5 | 24k samples | HUMAN-3.00-0.9981 — stop |
Returns at first confident detection (4k–8k). Accuracy ~94–96%, latency 0.5–1 second. Best for high-volume, latency-sensitive routing.
{"config": {"sample_rate": 8000, "VID": "...", "immediate_detection": true}}
Accepted truthy values for immediate_detection: JSON true, integer 1, float 1.0, or strings "true" / "yes" / "on" / "1" (case-insensitive). Falsy: false, 0, 0.0, "false" / "no" / "off", or absent. On server builds older than 2026-07-31 only the unquoted JSON boolean works — other encodings are silently ignored and the mode stays off. If the flag appears to have no effect, verify the encoding first.
| Chunk | Buffer | Reply |
|---|---|---|
| 1 | 4k samples | HUMAN-0.50-0.8740 — stop |
Instead of silent "" replies between checkpoints, the server streams a STAGE-… progress frame after each cascade stage completes. The final result frame is identical to standard mode — no parsing changes required for existing consumers. Combine with standard mode for diagnostic visibility; combine with immediate mode for fast routing with progress frames.
{"config": {"sample_rate": 8000, "VID": "...", "stage_results": true}}
Format: STAGE-<stage>-<CLS>-<duration>-<confidence>. Stage frames occupy the same reply slot as empty strings. A frame always starts with STAGE- and is never a valid final classification token. If multiple cascade stages complete between two audio chunks, they are semicolon-joined in a single frame.
| Chunk | Buffer | Reply (stage_results: true) |
|---|---|---|
| 1 | 4k samples | STAGE-1k-AMD-0.12-0.55;STAGE-2k-AMD-0.25-0.61;STAGE-3k-AMD-0.37-0.68;STAGE-4k-AMD-0.50-0.72 — keep going |
| 2 | 8k samples | STAGE-8k-AMD-1.00-0.79 — keep going |
| 3 | 12k samples | STAGE-12k-AMD-1.50-0.88 — keep going |
| 5 | 24k samples | WELCOMEVMAMD-3.00-0.9734 — stop (final) |
msg = await ws.recv()
if not msg:
pass # empty — keep streaming (standard mode)
elif msg.startswith("STAGE-"):
for frame in msg.split(";"):
_, stage, cls, dur, conf = frame.split("-", 4)
print(f" {stage}: {cls} @ {dur}s ({float(conf):.0%})") # optional
# keep streaming regardless
else:
cls, dur, conf = msg.split("-") # final result
break
import asyncio, json, websockets, numpy as np, soundfile as sf
async def detect(audio_file, api_key, immediate=False):
uri = "ws://api.amdy.io:2700"
async with websockets.connect(uri, extra_headers={"X-API-Key": api_key}) as ws:
# 1) Send config
await ws.send(json.dumps({"config": {
"sample_rate": 8000,
"VID": f"CALL_{int(asyncio.get_event_loop().time()*1000)}",
"immediate_detection": immediate,
}}))
# 2) Load and resample to 8 kHz int16
audio, sr = sf.read(audio_file)
if audio.ndim > 1: audio = audio.mean(axis=1)
if sr != 8000:
from scipy import signal
audio = signal.resample(audio, int(len(audio)*8000/sr))
pcm = (audio * 32767).astype(np.int16)
# 3) Stream 1-second chunks, stop on non-empty reply
CHUNK = 8000
for i in range(0, len(pcm), CHUNK):
await ws.send(pcm[i:i+CHUNK].tobytes())
try:
res = await asyncio.wait_for(ws.recv(), timeout=0.5)
except asyncio.TimeoutError:
continue
if res:
return parse(res)
# 4) Audio exhausted — force a result
await ws.send(json.dumps({"eof": 1}))
return parse(await ws.recv())
def parse(res):
res = res if isinstance(res, str) else res.decode()
cls, dur, conf = res.split("-")
return {"classification": cls, "duration": float(dur), "confidence": float(conf)}
# Usage
r = asyncio.run(detect("call.wav", "YOUR_API_KEY"))
print(r["classification"], f'{r["confidence"]:.2%}')
is_human = r["classification"] == "HUMAN"
const WebSocket = require('ws');
const fs = require('fs');
const wav = require('node-wav');
function detect(audioFile, apiKey, immediate = false) {
return new Promise((resolve, reject) => {
const ws = new WebSocket('ws://api.amdy.io:2700', {
headers: { 'X-API-Key': apiKey }
});
const CHUNK = 8000;
let pcm, offset = 0;
const next = () => {
if (offset < pcm.length) {
ws.send(pcm.slice(offset, offset += CHUNK).buffer);
} else {
ws.send(JSON.stringify({ eof: 1 })); // force result
}
};
ws.on('open', () => {
ws.send(JSON.stringify({
config: { sample_rate: 8000, VID: `CALL_${Date.now()}`, immediate_detection: immediate }
}));
const d = wav.decode(fs.readFileSync(audioFile));
pcm = Int16Array.from(d.channelData[0], x => Math.max(-32768, Math.min(32767, x * 32767)));
next();
});
ws.on('message', (data) => {
const res = data.toString();
if (res) {
const [cls, dur, conf] = res.split('-');
ws.close();
resolve({ classification: cls, duration: +dur, confidence: +conf });
} else {
next();
}
});
ws.on('error', reject);
});
}
detect('call.wav', 'YOUR_API_KEY')
.then(r => console.log(r.classification, (r.confidence * 100).toFixed(2) + '%'));
#!/bin/bash
# Register this server's IP — run once during setup
API_KEY="YOUR_API_KEY"
DESCRIPTION="$(hostname)"
curl -sfX POST https://app.amdy.io/api/v1/ips/register \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{"description": "$DESCRIPTION"}" | python3 -m json.tool
ffmpeg -i input.mp3 -ar 8000 -ac 1 -f s16le -acodec pcm_s16le output.raw
import soundfile as sf, numpy as np
from scipy import signal
def to_pcm(path):
a, sr = sf.read(path)
if a.ndim > 1: a = a.mean(axis=1)
if sr != 8000: a = signal.resample(a, int(len(a) * 8000 / sr))
return (a * 32767).astype(np.int16)
SILENCEAMD results. Always verify format before streaming.
| Code | Meaning | Action |
|---|---|---|
1000 | Normal closure (result delivered) | No action needed |
1001 | Server maintenance | Retry after a few seconds |
1006 | Connection lost | Retry with exponential backoff |
1008 | Policy violation (IP not whitelisted) | Register the IP via POST /api/v1/ips/register |
1011 | Server error | Retry; contact support if persistent |
| Symptom | Cause | Fix |
|---|---|---|
| Empty replies indefinitely | Wrong audio format or no EOF sent | Verify 8 kHz/16-bit/mono; send {"eof":1} when audio ends |
| Connection refused / 1008 | Source IP not registered | Call POST /api/v1/ips/register from that server |
| Slow detection | Standard mode running full cascade | Enable immediate_detection: true |
| Registration 502 | Internal sync issue | Retry once; include the hint field when emailing support |
import time
def detect_with_retry(audio_file, api_key, max_retries=3):
for attempt in range(max_retries):
try:
return asyncio.run(detect(audio_file, api_key))
except Exception as e:
if attempt == max_retries - 1: raise
time.sleep(2 ** attempt) # 1s, 2s, 4s
| Channel | Details |
|---|---|
| Portal | app.amdy.io — manage keys, IPs, billing, analytics |
| [email protected] |
client_id and hint fields from the 502 response body.