Voicemail Detection#

When making automated phone calls, it’s useful to know if you got an answering machine or an actual person. Twilio’s API has pretty good detection built in, and in this example we’ll show how to use that.

The code shown here is from the llm_testing_app example in the GitHub repo.

Initiating Voicemail Detection#

Twilio call this “Answering machine detection”. I don’t think anyone has an answering machine anymore, but whatever.

Lines 8 through 10 show the detection parameters.

 1@app.route("/call/<path:endpoint>", methods=["GET"])
 2async def outbound_call(endpoint):
 3    phone_number = request.args.get("phone")
 4    call_instance = await current_app.twilio_client.calls.create_async(
 5        from_=app.config["TWILIO_PHONE_NUMBER"],
 6        to=phone_number,
 7        url=f"https://{current_app.domain}/twiml/{endpoint}",
 8        async_amd=True,
 9        machine_detection="DetectMessageEnd",
10        async_amd_status_callback=f"https://{current_app.domain}/machine_detect",
11    )
12    logger.info(f"Initiating outbound call. SID: {call_instance.sid}")
13    current_app.current_calls[call_instance.sid] = CallQueues()
14    return {"callSid": call_instance.sid}

In a voice app, we generally want asynchronous detection, so when the call connects we will immediately get the audio and start processing. The detection happens in the background and we will get notified if an answering machine is detected.

Setting machine_detection to DetectMessageEnd tells Twilio to call the webhook when the voicemail greeting has completed, so if you want to play a canned message, you can start with that immediately.

The final parameter async_amd_status_callback sets the webhook that Twilio will call when the detection is done. It will always hit this endpoint, giving you a response of ‘human’, ‘amd’, or ‘unknown’.

Implementing the Detection WebHook#

In this case, we just convert the detection into a message and put it on the inbound queue for that call.

 1@app.route("/machine_detect", methods=["POST"])
 2async def machine_detect_webhook():
 3    form = await request.form
 4    event = form_data_to_dict(form)
 5    call_id = event["CallSid"]
 6    event = AnsweringMachineDetection(
 7        call_id=call_id,
 8        answered_by=event["AnsweredBy"],
 9        time_since_start=float(event["MachineDetectionDuration"]) / 1000,
10    )
11    call = current_app.current_calls.get(call_id, None)
12    if call:
13        await call.inbound.put(event)
14    return "Ok"

This queue can be merged with other Twilio events in the call handler so you can process the call however you want.

 1@app.websocket("/ws")
 2async def handle_call_audio():
 3    stream = quart_websocket_source()
 4    stream = map_str_to_json_step(stream)
 5    stream = filter_step(stream, lambda x: x["event"] != "connected")
 6    stream = twilio_check_sequence_step(stream)
 7    
 8    # Extract the callSid and use that to find the right queue.
 9    stream, call_sid_f = extract_value_step(
10        stream, value=lambda x: x["start"]["callSid"]
11    )
12    inbound_queue_f = map_future(call_sid_f, lambda x: current_app.current_calls[x].inbound)    
13    
14    vm_detection_source = queue_source(inbound_queue_f)
15    stream = merge_step(stream, vm_detection_source)
16
17    # From here the AnsweringMachineDetection event will be in the stream just like CallStarted and CallEnded events.
18    # ...