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:
{
"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. Your signing secret (whsec_…) is in the dashboard under Webhooks. Headers:
webhook-id— the detection IDwebhook-timestamp— Unix secondswebhook-signature—v1,+ base64 HMAC-SHA256 of{id}.{timestamp}.{raw body}, keyed with the base64-decoded part of the secret afterwhsec_
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.