AMDY API reference
AMDY.IO exposes a real-time AMD Detection WebSocket for streaming call audio, plus a small set of API-key authenticated REST routes for registration, configuration and per-IP settings. This page documents only what is verified in the AMDY source tree as of 2026-08-28. Anything not verified is flagged "contact us" so you do not integrate against a guess.
What endpoints does AMDY expose?
Two surfaces, both public-internet-facing:
- A WebSocket endpoint for live AMD detection: your telephony server streams 8 kHz PCM and receives a classification.
- A set of REST routes under /api/v1/* for account and IP management, used by install scripts and polled by the detection servers.
What is the AMD Detection WebSocket endpoint?
| Field | Value |
|---|---|
| URL | ws://api.amdy.io:2700 |
| Transport | Plain WebSocket (no TLS variant verified in source) |
| Auth | X-API-Key header on connect, read from /etc/amdy/api-key |
| Audio format | 8000 Hz, 16-bit, mono, raw PCM (signed little-endian) |
The first text frame must be a config frame: {"config":{"sample_rate":8000,"VID":"<caller id name or Unknown>"}}. After that, binary PCM frames follow, one per chunk. A final text frame {"eof":1} closes the stream and forces a result.
FreeSWITCH exception: mod_audio_fork cannot send custom headers on the WebSocket upgrade, so fork connections carry no X-API-Key and are gated by IP allowlist instead.
A TLS (wss://) variant is not present in any AMDY source in this repository. Contact us to confirm availability for your deployment.
What REST routes exist under /api/v1?
Four API-key authenticated routes. All four use the same auth check and are scoped to the client that owns the key.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/ips/register | Register a server IP and activate the account |
| GET | /api/v1/config | Client-level detection config, polled by detection servers |
| GET | /api/v1/client-settings | Per-IP settings bundle (rate-limited) |
| GET | /api/v1/ips | List IPs owned by the client, with their settings |
There is also a public, unauthenticated health endpoint: GET /api/health. It returns platform-wide stats (status, detections_7d, human_pct_7d, p95_latency_ms, updated_at), cached for 5 minutes, with no per-client data.
How do I authenticate?
Every /api/v1/* route accepts either:
- Authorization: Bearer <key>
- X-API-Key: <key>
The key is looked up in api_keys. Unknown key returns 401; a key whose status is not active returns 403 on register and 401 on the read routes.
Keys are stored on installed servers at /etc/amdy/api-key (mode 600) and referenced from /etc/amdy/amdy.conf.
What is the API key format?
AMDY keys are not 64-character hex strings. The real format, verified in the minting code, is:
- On signup: amd_live_ + crypto.randomBytes(12) as hex (24 hex chars, 32 chars total including the prefix).
- On manual creation via the portal: amd_live_ + crypto.randomBytes(16) as hex (32 hex chars, 40 chars total).
The legacy sk-$ENV-* prefix has been retired. If your integration still expects a 64-character hex string, please contact us before upgrading.
How do I register a server IP?
POST /api/v1/ips/register activates the account and mirrors the IP into TimescaleDB for analytics attribution.
| Body field | Required | Notes |
|---|---|---|
| ip | no | Validated with net.isIP; auto-detected from X-Forwarded-For / X-Real-IP when absent |
| description | no | Defaults to Installed via installamd-v2.sh, truncated to 191 chars |
Success (200):
{
"ok": true,
"activated": true,
"account_activated_now": true,
"client_id": 1234,
"ip": "203.0.113.4",
"registered": { "ip": "203.0.113.4", "status": "registered" }
}registered.status is one of registered, already_registered or existing. Errors return 400 (invalid IP), 401 (missing/invalid key), 403 (key not active) or 502 (upstream registration failure with error, code, client_id, ip and hint fields).
How do I read my detection configuration?
GET /api/v1/config returns client-level settings, polled by detection servers roughly every 60 seconds:
{
"clientId": 1234,
"detectionSensitivity": 3,
"maxDetectionMs": 8000,
"updatedAt": "2026-08-01T12:00:00.000Z"
}detectionSensitivity defaults to 3, maxDetectionMs defaults to 8000.
How do I read per-IP settings?
GET /api/v1/client-settings returns one entry per IP the client owns, plus a default entry. It is rate-limited to 20 requests per minute per key (429 with Retry-After).
{
"version": 1722470400,
"generated_at": "2026-08-28T09:00:00.000Z",
"clients": {
"default": { "max_detection_time": 8, "detection_mode": "balanced" },
"203.0.113.4": {
"max_detection_time": 6,
"detection_mode": "aggressive",
"stage_sensitivity": { "1k": 3, "32k": 10 }
}
}
}| Setting key | Values |
|---|---|
| max_detection_time | 0.5 to 10.0, clamped |
| detection_mode | aggressive, balanced, conservative |
| greeting_grace | boolean |
| immediate_detection | boolean |
| stage_results | boolean |
| short_no_greeting | boolean |
| stage_sensitivity | optional object, any subset of 1k, 2k, 3k, 4k, 8k, 12k, 16k, 24k, 32k mapped to an integer 1-10. 1 = most aggressive at that stage, 5 = current production, 10 = stage disabled. Omitted stages fall back to detection_mode. |
stage_sensitivity is per-stage, not all-or-nothing: an explicit stage level wins over the detection_mode profile, which in turn wins over the shipped defaults. Levels carry no thresholds — the level-to-threshold ladder lives on the detection servers, so a retune keeps every level’s meaning stable.
Each key is omitted when NULL. Ownership is checked against firewall_production.client_ips; no rows returns 200 with an empty clients object.
How do I list my registered IPs?
GET /api/v1/ips returns the same rate limit (20/min) and the same per-IP settings keys as /api/v1/client-settings. Responses are never cached (Cache-Control: no-store).
{
"client_id": 1234,
"generated_at": "2026-08-28T09:00:00.000Z",
"ips": [
{
"ip": "203.0.113.4",
"status": "active",
"settings": { "max_detection_time": 6, "detection_mode": "aggressive" }
}
]
}Reseller API: how do I manage clients programmatically?
Resellers get a separate key type, amd_rsl_…, created under Reseller → API access in the portal (owner only; shown once). It authenticates the same way as a client key, Authorization: Bearer amd_rsl_…, at 60 requests per minute, and every call is scoped to clients in your reseller org. A client id outside your org returns 403.
| Method | Route | Purpose |
|---|---|---|
| GET | /api/v1/reseller/clients | List clients (id, name, status, active IP count) |
| POST | /api/v1/reseller/clients | Create a client: {name, email, password?}; returns id, temp password, Sandbox key |
| GET | /api/v1/reseller/clients/{clientId}/ips | List a client's registered IPs |
| POST | /api/v1/reseller/clients/{clientId}/ips | Register an IP for that client: {ip, description?} |
| DELETE | /api/v1/reseller/clients/{clientId}/ips | Remove an IP: {ip} (body or ?ip=) |
| GET | /api/v1/reseller/clients/{clientId}/api-keys | List the client's AMD API keys (prefixes only) |
| POST | /api/v1/reseller/clients/{clientId}/api-keys | Create an AMD key: {name, environment?}; full key returned once |
| DELETE | /api/v1/reseller/clients/{clientId}/api-keys/{keyId} | Revoke an AMD key |
Provisioning a new customer end to end:
# 1. create the client
curl -X POST https://app.amdy.io/api/v1/reseller/clients \
-H "Authorization: Bearer amd_rsl_..." -H "Content-Type: application/json" \
-d '{"name": "Acme Dialers", "email": "[email protected]"}'
# -> {"client": {"clientId": 6000123, "email": "...", "tempPassword": "...", "sandboxApiKey": "amd_live_..."}}
# 2. register the dialer's IP for that client
curl -X POST https://app.amdy.io/api/v1/reseller/clients/6000123/ips \
-H "Authorization: Bearer amd_rsl_..." -H "Content-Type: application/json" \
-d '{"ip": "203.0.113.10", "description": "Dialer 1"}'
# -> {"registered": {"clientId": 6000123, "ip": "203.0.113.10", "status": "registered"}}
# 3. mint a production AMD key for the install script
curl -X POST https://app.amdy.io/api/v1/reseller/clients/6000123/api-keys \
-H "Authorization: Bearer amd_rsl_..." -H "Content-Type: application/json" \
-d '{"name": "Dialer 1", "environment": "production"}'
# -> {"apiKey": {"id": "...", "key": "amd_live_...", "environment": "production", "status": "active"}}Errors return {"error": "..."} with 400 (bad input), 401 (missing or revoked key), 403 (not your client), 409 (email or IP already registered) or 429 (rate limit, with Retry-After).
Alerts: email and webhook
AMDY checks every registered IP hourly. An IP that had traffic in the last 14 days but none in the last 24 hours is quiet: the client's billing contact and the reseller owner get one email per client (all quiet IPs listed), and one more when traffic resumes. Resellers can also receive the same events as a signed webhook, set under Reseller → Alert webhook or with PUT /api/v1/reseller/webhook {url} (https only; the signing secret is returned once). POST the same route to send a webhook.test event; DELETE removes it.
POST <your url>
X-AMDY-Event: client.ip.quiet # or client.ip.recovered, webhook.test
X-AMDY-Timestamp: 1789500000
X-AMDY-Signature: sha256=<hex HMAC-SHA256 of "<timestamp>.<raw body>" using your secret>
{
"event": "client.ip.quiet",
"timestamp": 1789500000,
"data": {
"client_id": 6000123,
"client_name": "Acme Dialers",
"ips": [{ "ip": "203.0.113.10", "last_seen_at": "2026-09-13T04:00:00.000Z" }]
}
}Verify by recomputing the HMAC over timestamp + "." + body and comparing in constant time; reject timestamps older than a few minutes. Deliveries time out after 10 s and are not retried; the last status is visible in the portal and via GET /api/v1/reseller/webhook.
What is the install command per platform?
| Platform | Install command |
|---|---|
| ViciDial / ViciBox | curl -fsSL https://download.amdy.io/installamd-v2.sh | bash -s <API_KEY> |
| Asterisk (custom dialer) | curl -sL https://download.amdy.io/installamd-asterisk.sh | bash -s -- <API_KEY> |
| FreeSWITCH | installamd-freeswitch.sh <API_KEY> --module /path/to/mod_audio_fork.so (no curl one-liner; module must be supplied) |
| Issabel | Not verified in source. Contact us for integration access. |
Every installer writes the key to /etc/amdy/api-key (mode 600) and calls POST /api/v1/ips/register with the host's detected public IP. On ViciBox, never chown to asterisk: Vicidial runs as root.
Frequently asked questions
Is there a wss:// endpoint?
Not verified in source. Only ws://api.amdy.io:2700 is documented in this repository. Contact us to confirm TLS availability for your network.
Can I revoke an API key over the public API?
No. Key management (create and revoke) is portal-only via /api/api-keys, authenticated by your NextAuth session. Revoke sets status='revoked'; the row is not deleted.
Can I delete a registered IP over the API?
No. IP removal is portal-only. The public API exposes register and read routes, not delete.
Is Issabel supported?
Not verified in source. Issabel is Asterisk-based, but AMDY ships no Issabel-specific installer or dialplan. Contact us for integration access.
What does /api/health return?
Platform-wide stats only: {"status":"operational|degraded","detections_7d":...,"human_pct_7d":...,"p95_latency_ms":...,"updated_at":"..."}. Cached for 5 minutes. No per-client data.
Ready to integrate?
Generate an API key from your AMDY.IO dashboard and run the installer for your platform. The free Sandbox plan includes 50,000 detections per month, no card.
Open the AMDY dashboardSupport
Questions about an endpoint not listed here? Email [email protected] with the route, the request you sent and the response you received.