Search code examples
javascriptc#twiliotwilio-apitwilio-twiml

Automatic Machine Detection workflow is different when used under <Dial><Number> vs when used in an outbound call using API


I cannot control the flow of a call created using Dial keyword inside of amdStatusCallback webhook I provide in the Number tag. I'm using C# Twilio library and Voice JavaScript SDK. Apologies for the long one in advance.

Use case:

  1. Start a call from browser to a phone number:
  2. When the called party picks up I want AMD to work, then:

2.1. If the result is human or unknown, a message should be said, stating that continuing the call indicates the called party's consent to be recorded. Then it should connect the caller in browser and the callee.

2.2. machine-start: no point in asking for consent, so it should connect immediately for the caller to leave a message.

2.3. fax: just disconnect. I'll let the caller know it's a fax.

The perfect scenario would be if there was asyncAMD flag in Number keyword like it exists in CallResource.CreateAsync, and the call would go the 2.1 route by default and change after, but for some reason there's no such thing. I can live with that though, it's not the main issue.

Here's what happens:

  • Twilio.device is initialized correctly with a token that contains a reference to a TwiML App on my account;
  • A request is made to the webhook from the TwiML App;
  • Here's the response:
<Response>
    <Dial callerId="{myTwilioPhoneNumber}" record="record-from-answer" answerOnBridge="true" ringTone="us">
        <Number machineDetection="Enable" amdStatusCallbackMethod="GET" amdStatusCallback="{amdUrl}" machineDetectionTimeout="5">*phone-number*</Number>
    </Dial>
</Response>

Here's what I tried to do in amdUrl:

  • Respond with a Say TwiML instruction. But it is completely ignored.
<Response>
    <Say language="en-US">Very long message stating that continuing the call indicates your consent to be recorded</Say>
</Response>
  • Send the same TwiML instuction using CallResource.UpdateAsync. Both using twiMl parameter and as a response to another webhook I provided in the url parameter. It worked BUT the call gets disconnected from the twilio device in browser. I tried to add <Dial><Client>{voiceSdkDeviceId}</Client></Dial> or <Dial><Client><Identity>{voiceSdkDeviceId}</Identity></Client></Dial> after the Say to at least somehow connect them back but it also didn't work. I see the child call created in the logs but it ends with No Answer. I double-checked the device id multiple times, I call device.register() in my JS, I see the registered event fired.

When I created outbound calls using CallResourse.CreateAsync for another task, I enabled AMD there as well, and it worked just as described in this tutorial. It's a very unpleasant surprise that it doesn't work like this using Dial.

Before I enabled AMD I had a webhook that responded with the same Say instruction. I provided url to it in the Number tag and had the message said while hearing ringing in browser and then got connected. But if the call goes to voicemail, the consent message may easily be longer than the voicemail's greeting and still be playing after the beep. So the caller would have no idea it went to voicemail, and the callee would receive a message that starts in the middle of the consent message followed by confused silence of the caller. I'd really like to avoid it. Please help.


Solution

  • So, I've turned everything over. List of problems and their respective solutions:

    1. Can't control calls created with <Dial><Number> from AMD webhooks:
    • Create the call to the number using API (CallResource.CreateAsync()), enable AMD there and provide the same webhook urls. I also was able to enable async AMD this way.
    1. Can't connect the call from above to Voice JS SDK device in browser (calls ended with No Answer):
    • Connect both the API call and Voice JS SDK device to a uniquely named Conference created for the two specifically.

    So, instead of <Dial><Number> on one side and <Dial><Client> on the other I have <Dial><Conference> on both sides.

    Problems with the new approach:

    1. I'll be charged more for it. But it works.
    2. The ringing sound was replaced by weird music:
    • Unfortunate but I can live with that for now. Will be able to change that with waitUrl parameter in future.
    1. If the callee hangs up before ever connecting to the conference, the caller will not know about it:
    • Add statusCallback url parameter to the CreateAsync, provide the conference name there. If a request with a completed status is received, end the conference through API:
    var conference = (await ConferenceResource.ReadAsync(friendlyName: conferenceName)).FirstOrDefault();
    if (conference != null)
        await ConferenceResource.UpdateAsync(conference.Sid, status: ConferenceResource.UpdateStatusEnum.Completed);