1. Remove Twilio AMD from call creation
For an outbound call created with the Twilio Node SDK, change the call options:machineDetection* tuning and asyncAmd* callback settings. Also remove
Twilio AMD attributes from a child leg or participant if your flow enables it there.
If your old flow used synchronous AMD, your answer URL will no longer wait
for that verdict. Decide whether to connect immediately or hold in your own
waiting flow until the PyAI callback arrives. PyAI does not pause or redirect
Twilio calls for you. See Twilio’s AMD modes.
2. Start a stream on the called-party leg
Have/outbound-twiml return this XML with content type text/xml.
Insert your PyAI key on the server; do not expose it in browser code.
<Start><Stream>, which continues to the next TwiML verb. A following verb
is required to keep the call alive. The pause here is only a test fixture; it
does not delay AMD. <Connect><Stream> instead blocks subsequent TwiML.
For the callee’s own call leg, inbound_track is their audio arriving at
Twilio. Do not attach this example blindly to an agent or parent leg; bridge
and conference topologies can put the called party on another leg. Use only the
called party’s audio. Twilio documents tracks and custom parameters here.
Set decision_timeout_ms to 3000 or 5000; it accepts 1000–15000 ms and starts
when PyAI accepts the authenticated stream start. Omit language for automatic
multilingual recognition. No separate STT stream is needed.
3. Adapt your AMD callback
The per-callwebhook receives JSON, not Twilio’s form-encoded callback.
Example payload excerpt:
The socket has an
event: "amd" envelope; the per-call HTTP callback does not.
Do not require body.event === "amd" in this receiver.
This callback also does not carry X-Twilio-Signature or the signed account
webhook header. Do not reuse Twilio signature validation for it. Use your own
receiver validation, or fetch the stored result with your PyAI key before a
sensitive routing action. The AMD reference
also describes the separate signed account completion webhook.
Acknowledge the HTTP request promptly after accepting it. Returning TwiML in
this response does not control the call. Use your existing call controller
or Twilio’s update-call API
to change the ongoing call. Make routing idempotent per call, particularly if
both PyAI webhook paths are enabled. Keep a fallback timer for a missing result.
4. Route by the actual result
Check these PyAI fields before adapting legacy routing. In particular,
answered_by_twilio: machine_end_other can represent sit_invalid; it is not
proof of a completed voicemail greeting. Screening is also a machine result
and should not automatically trigger a hangup.
There is one result per stream. A later human takeover does not produce a
second callback. For integrations that wait for a decision, include webhook
transit time in the fallback timer and preserve the call while waiting.
5. Test before switching routing
Try a live human, quiet or long greeting, voicemail, screening and silence. For each call, confirm the called-partyCallSid, echoed correlation values,
result, and end-to-end receipt time. Record engine_version and rule_id.
If comparing both detectors on the same call, run PyAI in observation mode in
your application: record its result without routing on it. Keep the media stream
alive independently of Twilio’s verdict so neither result truncates the other’s
input. Disagreement alone does not establish which detector was correct.
After testing, choose one detector to control routing and remove the old AMD
configuration. You can keep Twilio’s normal call-status callbacks.
Troubleshooting
See the AMD guide and reference for
full fields, SDK examples, signing and parameter limits. Twilio requires each
custom parameter’s name and value together to be under 500 characters, even
where PyAI’s direct-stream limits are larger.