Twilio
Twilio runs the TwiML for an outbound call when the callee answers. Return <Start><Stream> 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 <Stream url> accepts neither headers nor a query string, so authenticate with a <Parameter>: 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.
await twilio.calls.create({
to: '+15551234567',
from: '+15557654321',
url: 'https://you.example/twilio/answered?lead=42',
});
2. Answer webhook: start the stream
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(`<Response>
<Start>
<Stream url="${stream.base_url}" track="inbound_track">
<Parameter name="token" value="${stream.token}"/>
</Stream>
</Start>
<Pause length="20"/>
</Response>`);
});
track="inbound_track" sends only the callee's side. The <Pause> 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.
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: '<Response><Dial><Client>rep</Client></Dial></Response>' });
} 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:
<Parameter name="api_key" value="pamd_live_…"/>plus<Parameter name="webhook_url" value="https://you.example/amd"/>. Other<Parameter>values are echoed back asmetadata. This exposes the key to Twilio's request logs; tokens are preferred. - Decision on the socket: if you relay the media yourself, add
<Parameter name="events" value="1"/>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.