# Patch AMD — full documentation > Patch AMD is an answering machine detection API for outbound calls. Stream the call audio from Twilio, Telnyx or any dialer and get one decision — human, voicemail, IVR, unavailable or silence — usually within a second of the greeting. Source: https://patchamd.com # Quickstart Patch AMD tells you who answered an outbound call: a **person**, a **voicemail**, an **IVR** (phone menu, hold queue, call screening), an **unavailable** line (not in service, mailbox full), or **silence**. You stream the callee's audio from the moment the call is answered; we send back one decision and close the stream. ## 1. Get an API key [Create an account](/signup), open **API keys** in the dashboard and create a key. It looks like `pamd_live_…` and is shown once. Keep it on your server. ## 2. Start a stream when the call is answered Pick your carrier: - **Twilio** — add `` to the TwiML your answer webhook returns. [Twilio guide](/docs/twilio) - **Telnyx** — call `streaming_start` on `call.answered`, or pass `stream_url` when you dial. [Telnyx guide](/docs/telnyx) - **Anything else** — open a WebSocket and send raw mu-law, A-law or 16-bit PCM frames. [Raw WebSocket guide](/docs/raw-websocket) Carriers can't send our API key as a header, so for them you first mint a **signed stream URL** on your server: ```bash curl -X POST https://api.patchamd.com/v1/streams \ -H "Authorization: Bearer $PATCH_AMD_KEY" \ -H "Content-Type: application/json" \ -d '{"webhook_url": "https://you.example/amd", "metadata": {"lead": "42"}}' ``` ```json { "id": "det_4f9Qm2LxV7cT1bZr8KpA", "url": "wss://api.patchamd.com/v1/stream?token=det_4f9Qm2LxV7cT1bZr8KpA.1794340000000.…", "base_url": "wss://api.patchamd.com/v1/stream", "token": "det_4f9Qm2LxV7cT1bZr8KpA.1794340000000.…", "expires_at": "2026-11-10T19:53:03.114Z" } ``` The URL works once and must be opened within 10 minutes. ## 3. Act on the decision We POST an `amd.decided` [webhook](/docs/webhooks) to your `webhook_url` (and, for raw streams, send the same event on the socket): ```json { "type": "amd.decided", "data": { "detection_id": "det_…", "result": "human", "reason": "person_waiting", "at_ms": 1620, "heard": "Hello?" } } ``` | result | What it means | Typical action | |---|---|---| | `human` | A person answered and is waiting | Connect a rep or agent | | `voicemail` | Voicemail greeting | Hang up, or leave a message | | `ivr` | Phone menu, hold queue, call screening | Hang up or navigate | | `unavailable` | Not in service, can't be completed, mailbox full or not set up | Hang up; clean the number | | `silence` | Nobody spoke | Hang up or retry | | `not_sure` | Could not tell, or our detector failed | **Treat as a person** | ## Timing A person is usually connected a fraction of a second after they finish their greeting (median 0.15 s on our [benchmark](/benchmark)). Recordings are usually caught mid-greeting, a median 3.4 s after pickup. If nobody speaks the result is `silence`, and every stream reaches a decision within seconds. ## Next - [API reference](/docs/api-reference) — REST endpoints, stream protocol, results and reasons - [Webhooks](/docs/webhooks) — signature verification - Machine-readable: [OpenAPI](/openapi.json), [AsyncAPI](/asyncapi.json), [llms-full.txt](/llms-full.txt) --- # Twilio Twilio runs the TwiML for an outbound call when the callee answers. Return `` from that webhook to fork the callee's audio to Patch AMD while the call continues, then act on the `amd.decided` webhook. Twilio's `` accepts neither headers nor a query string, so authenticate with a ``: mint a signed stream token per call and pass it as `token`. ## 1. Place the call Turn **off** Twilio's own AMD (`machineDetection`) — Patch AMD replaces it. ```js await twilio.calls.create({ to: '+15551234567', from: '+15557654321', url: 'https://you.example/twilio/answered?lead=42', }); ``` ## 2. Answer webhook: start the stream ```js app.post('/twilio/answered', async (req, res) => { const stream = await fetch('https://api.patchamd.com/v1/streams', { method: 'POST', headers: { authorization: `Bearer ${process.env.PATCH_AMD_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ webhook_url: 'https://you.example/amd', metadata: { lead: req.query.lead } }), }).then(r => r.json()); res.type('text/xml').send(` `); }); ``` `track="inbound_track"` sends only the callee's side. The `` keeps the call open while we listen; the decision arrives long before it ends. ## 3. Route the call on the decision The webhook's `data.call_id` is the Twilio `CallSid`. ```js app.post('/amd', express.raw({ type: 'application/json' }), async (req, res) => { const event = verify(req); // see /docs/webhooks res.sendStatus(204); const { result, call_id } = event.data; if (result === 'human' || result === 'not_sure') { await twilio.calls(call_id).update({ twiml: 'rep' }); } else { await twilio.calls(call_id).update({ status: 'completed' }); } }); ``` ## Alternatives - **No REST call per dial:** put your API key in the TwiML instead of a token: `` plus ``. Other `` values are echoed back as `metadata`. This exposes the key to Twilio's request logs; tokens are preferred. - **Decision on the socket:** if you relay the media yourself, add `` and read events from the socket (Twilio itself ignores them). ## Audio Twilio Media Streams send 8 kHz mu-law (`audio/x-mulaw`) in 20 ms frames, which we take as-is. --- # Telnyx Telnyx streams call audio to a WebSocket URL you give it. Mint a signed stream URL on your server and hand it to Telnyx; Patch AMD posts the decision to your webhook. ## Option A: start streaming on answer ```js // Your Telnyx webhook, on call.answered for the outbound leg const stream = await fetch('https://api.patchamd.com/v1/streams', { method: 'POST', headers: { authorization: `Bearer ${process.env.PATCH_AMD_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ webhook_url: 'https://you.example/amd', metadata: { lead: '42' } }), }).then(r => r.json()); await fetch(`https://api.telnyx.com/v2/calls/${callControlId}/actions/streaming_start`, { method: 'POST', headers: { authorization: `Bearer ${process.env.TELNYX_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ stream_url: stream.url, stream_track: 'inbound_track' }), }); ``` ## Option B: stream from dial Pass the same URL on the Dial command so no audio is missed between answer and `streaming_start`: ```json { "to": "+15551234567", "from": "+15557654321", "connection_id": "…", "stream_url": "", "stream_track": "inbound_track" } ``` The socket opens at dial; Patch AMD starts the clock at Telnyx's `start` message, which arrives on answer. Turn **off** `answering_machine_detection` on the Dial. ## Route the call on the decision `data.call_id` is the Telnyx `call_control_id`. ```js const { result, call_id } = event.data; if (result === 'human' || result === 'not_sure') await bridgeToRep(call_id); else await telnyxAction(call_id, 'hangup'); ``` ## Audio We read the codec from Telnyx's `media_format`: PCMU and PCMA at 8 kHz, or L16 at 16 kHz. Other codecs fail open (`not_sure`, `detector_failed`, not billed). --- # Raw WebSocket If you control the media (FreeSWITCH, Asterisk, LiveKit, Pipecat, your own SIP stack), open a socket and send binary audio frames. Decision events come back on the same socket. ``` wss://api.patchamd.com/v1/stream?encoding=mulaw&sample_rate=8000 Authorization: Bearer pamd_live_… ``` | Query | Values | |---|---| | `encoding` | `mulaw`, `alaw` or `linear16` (16-bit little-endian PCM) | | `sample_rate` | `8000` (default), `16000`, `24000`, `44100`, `48000` | | `transcripts` | `1` to also receive `transcript` events with the words heard so far | | `webhook_url` | optional: also send `amd.decided` there | Can't set headers? Mint a URL with `POST /v1/streams` and `"encoding": "mulaw"` — the returned `url` carries the token and the codec. ## Protocol 1. Connect **when the call is answered**. The first audio frame starts the clock. 2. Send the callee's audio only (not your own outbound audio), ideally in 20 ms frames, in real time. 3. Receive events: ```json {"type":"connected","detection_id":"det_…"} {"type":"started","detection_id":"det_…","carrier":"raw","encoding":"mulaw/8000"} {"type":"transcript","detection_id":"det_…","text":"Hello?","finished":true} {"type":"decision","detection_id":"det_…","result":"human","reason":"person_waiting","at_ms":1620,"heard":"Hello?"} ``` 4. After `decision` the server closes the socket with code 1000. To stop early (the callee hung up), send `{"type":"stop"}` or close the socket; nothing is billed without a decision. ## Example (Node) ```js import WebSocket from 'ws'; const ws = new WebSocket('wss://api.patchamd.com/v1/stream?encoding=mulaw&sample_rate=8000', { headers: { authorization: `Bearer ${process.env.PATCH_AMD_KEY}` }, }); ws.on('open', () => call.on('audio', frame => ws.send(frame))); ws.on('message', data => { const event = JSON.parse(data); if (event.type === 'decision') route(event.result); }); ``` ## Close codes | Code | Meaning | |---|---| | 1000 | Decided | | 4401 | Bad credentials in a Twilio `start` message | | 4408 | No audio within 90 s of connecting | | 4429 | Concurrent stream limit reached | Bad credentials on connect are rejected before the upgrade with HTTP 401; a reused token with 409; rate limits with 429. --- # Webhooks Give a `webhook_url` (HTTPS, public address) when you mint a stream or connect with an API key. When the detection decides, we POST: ```json { "type": "amd.decided", "timestamp": "2026-11-10T19:43:03.114Z", "data": { "detection_id": "det_4f9Qm2LxV7cT1bZr8KpA", "result": "voicemail", "reason": "recording_detected", "at_ms": 2944, "heard": "Hi, you've reached Sam, please leave", "carrier": "telnyx", "call_id": "v3:…", "stream_id": "…", "metadata": { "lead": "42" } } } ``` `heard` is the words heard when the decision was made. It is sent to you and never stored by us. ## Delivery Any 2xx acknowledges. Otherwise we retry after 2 s and 10 s (not on 4xx other than 408 and 429). Each attempt times out after 5 s. Redirects are not followed. Delivery status shows on the detection (`webhook_status`). ## Verify the signature Webhooks follow [Standard Webhooks](https://www.standardwebhooks.com/). Your signing secret (`whsec_…`) is in the dashboard under **Webhooks**. Headers: - `webhook-id` — the detection ID - `webhook-timestamp` — Unix seconds - `webhook-signature` — `v1,` + base64 HMAC-SHA256 of `{id}.{timestamp}.{raw body}`, keyed with the base64-decoded part of the secret after `whsec_` ```js import { createHmac, timingSafeEqual } from 'node:crypto'; export function verify(req) { const id = req.get('webhook-id'), ts = req.get('webhook-timestamp'); if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('stale'); const key = Buffer.from(process.env.PATCH_AMD_WEBHOOK_SECRET.replace(/^whsec_/, ''), 'base64'); const expected = 'v1,' + createHmac('sha256', key).update(`${id}.${ts}.${req.body}`).digest('base64'); const ok = req.get('webhook-signature').split(' ').some(sig => sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected))); if (!ok) throw new Error('bad signature'); return JSON.parse(req.body); } ``` The `standardwebhooks` packages (Node, Python, Go, Ruby, PHP…) verify these headers directly. --- # API reference Base URL: `https://api.patchamd.com`. Authenticate with `Authorization: Bearer pamd_live_…` (or `X-API-Key`). Machine-readable specs: [OpenAPI 3.1](/openapi.json) for REST, [AsyncAPI 3.0](/asyncapi.json) for the WebSocket. ## POST /v1/streams Mint a single-use signed stream URL (valid 10 minutes) for dialers that can't send headers. | Field | Type | | |---|---|---| | `webhook_url` | string | HTTPS URL for `amd.decided` | | `metadata` | object | Up to 10 string values (≤256 chars), echoed in the webhook. Identifiers only, not call content. | | `encoding` | `mulaw` \| `alaw` \| `linear16` | Raw streams only | | `sample_rate` | integer | Raw streams only; default 8000 | | `events` | boolean | Also send events on a Telnyx/Twilio socket | Returns `201` with `id`, `url` (with token), `base_url` (without query, for Twilio), `token`, `expires_at`. ## GET /v1/detections/{id} ```json { "id": "det_…", "status": "decided", "carrier": "twilio", "result": "human", "reason": "person_waiting", "at_ms": 1620, "billable": true, "metadata": {}, "webhook_status": "delivered", "created_at": "…", "decided_at": "…" } ``` `status`: `pending` (minted), `streaming`, `decided`, or `ended` (closed before a decision — not billed). The words heard are not stored, so `heard` is not here. ## GET /v1/usage Billable detections per UTC day and key. Query: `from` (inclusive, default first of the month), `to` (exclusive, default tomorrow), `key=this` to limit to the calling key. ```json { "from": "2026-11-01", "to": "2026-11-11", "total": 2, "days": [{ "date": "2026-11-10", "key_id": "key_…", "detections": 2 }] } ``` ## WebSocket /v1/stream `wss://api.patchamd.com/v1/stream`. Accepts Telnyx media streaming, Twilio Media Streams (told apart by the `start` message), or raw binary audio when `encoding` is in the query. See the [raw guide](/docs/raw-websocket) for events and close codes. Authentication, in order: `?token=` (signed), `Authorization` / `X-API-Key` header, `?api_key=` (discouraged), or for Twilio a `token` / `api_key` ``. ## Results | result | Meaning | |---|---| | `human` | A live person who has stopped talking and is waiting | | `voicemail` | A voicemail or answering-machine greeting | | `ivr` | Phone menu, auto-attendant, hold queue, recording notice, call screening | | `unavailable` | Not in service, cannot be completed, mailbox full or not set up | | `silence` | Nobody spoke after the answer | | `not_sure` | Could not tell, or the detector failed — treat as a person | ## Reasons | reason | Meaning | |---|---| | `person_waiting` | A person spoke and is waiting for a reply ("Hello?") | | `recording_detected` | What was said could only come from a recording or automated system | | `inconclusive` | Speech that could not be placed either way (`not_sure`) | | `no_speech` | Nobody spoke | | `time_limit` | The listening window ended while the speaker was still talking (→ `voicemail`) | | `detector_failed` | Something failed on our side. Not billed. | ## Limits Per key: 600 requests and stream connects per minute, 100 concurrent streams (ask us to raise them). Over a limit: HTTP 429, or close code 4429 for a Twilio start. Revoked keys stop working within 30 seconds. ## Errors REST errors are JSON: `{"error": "invalid_request", "message": "…"}` with status 400, 401, 403, 404 or 429. --- ## FAQ ### What is Patch AMD? An answering machine detection (AMD) API for outbound calls. You stream the callee’s audio from the moment the call is answered; a real-time pipeline understands what is said, and Patch AMD returns one decision: human, voicemail, ivr, unavailable, silence or not_sure. ### Which carriers does it work with? Any that can stream call audio over a WebSocket. Twilio Media Streams and Telnyx media streaming work out of the box; anything else can send raw mu-law, A-law or 16-bit PCM frames. ### How fast is the decision? On the OpenAMD English benchmark, people were connected a median 0.15 seconds after they stopped speaking (90th percentile about 1 second), and every recording was identified before it finished playing, a median 3.4 seconds after pickup. ### How accurate is it? On the OpenAMD dataset’s English subset (128 clips: 85 people, 43 recordings), Patch AMD scored 96.9% with no recordings connected to a rep. The dataset’s best published model scored 96.1% on the same clips. ### What does it cost? $0.007 per answered detection, with volume tiers down to $0.004 and the first 250 detections each month free. Calls that are never answered, and detections that fail on our side, are not charged. ### Do you store call audio or transcripts? No. Audio is processed in real time and discarded; the words heard are sent to you in the decision event and webhook and never written to our database or logs. We keep the decision, its timing and your metadata for usage and billing. ### What happens if the detector fails? It fails open: the decision is not_sure with reason detector_failed, which you should treat as a person, and it is not billed. ### Does it work in languages other than English? Patch AMD is tuned and benchmarked for English. Other languages are available on request; they have not been benchmarked yet.