I am trying to set up a hunt group with Twilio Twiml
Do I have to set up a different twimlbin for each number in the hunt group?
Or is there a way to join all this together into a single Twimlbin?
Twimlbin 1:
<Response>
<Dial
action="http://www.ourapp.com/webhook;FailUrl=/Twimlbin 2"
timeout="10"
callerId="555-555-5555">
NUMBER1
</Dial>
</Response>
Twimlbin 2:
<Response>
<Dial
action="http://www.ourapp.com/webhook;FailUrl=/Twimlbin 3"
timeout="10"
callerId="555-555-5555">
NUMBER2
</Dial>
</Response>
... Repeat N times for each agent ...
Thank you :-)
Twilio developer evangelist here.
TwiML Bins are great for static bits of TwiML, but your use case needs a bit more than that.
I recommend you check out Twilio Functions which allow you to run Node.js code in Twilio's infrastructure. I've built and tested a version of this that works with Twilio Functions.
Here's an explanation of how it works:
Start with an array of your numbers:
const numbers = [...];
Then, in the function, check if the callstatus is completed and hang up if it is.
exports.handler = function(context, event, callback) {
const response = new Twilio.twiml.VoiceResponse();
if (event.DialCallStatus === "complete" || event.finished) {
// Call was answered and completed or no one picked up
response.hangup();
} else {
If it's not, we work out the next number to call. If you have the next number in the URL. If you do save it to a variable, otherwise pick the first number in the array:
const numberToDial = event.nextNumber ? event.nextNumber : numbers[0];
Then, assign the next number to dial from the array.
let url;
const currentNumberIndex = numbers.indexOf(numberToDial);
if (currentNumberIndex + 1 === numbers.length) {
// no more numbers to call after this.
url = "/hunt?finished=true";
} else {
const nextNumber = numbers[currentNumberIndex + 1];
url = "/hunt?nextNumber=" + encodeURIComponent(nextNumber);
}
Then generate the TwiML to Dial the next number and pass the URL as the action. You can add your own URL as the statusCallbackUrl to keep a track of the statuses.
const dial = response.dial({ action: url });
dial.number({ statusCallback: "https://yourapp.com/statuscallback" }, numberToDial);
}
callback(null, response);
}
I can't promise this will work, but I hope you see where I'm going with it. Let me know if it helps at all.
Related
So here's the logic of what I am working on:
Someone calls my Twilio Number I use the dial twiml to forward to a cell phone
I use gather and play a whisper to the operator answering the phone (so his cell phone).
The operator has a choice - press 1 to accept, press 2 (transfers to
a different agent).
step 3 is where I am having trouble I am using the code below:
$call = $twilio->calls($CallSid)
->update([
"method" => "POST",
"url" => "http:www.example.com/directcall.php"
]
);
Here's the problem it is modifying the call but it's redirecting the operators phone number instead of the person who is calling in. So the operator is getting redirected to the other operator and the customer is being hung up on. I tried using the parentcallsid too but that doesn't seem to work either.
Any ideas what I am doing wrong?
so just to be clear I want the flow to work like this:
Customer calls phone number -> redirects to designated operator -> if designated operator presses 2 it redirects the customer to operator 2 and disconnects operator 1 from the call. Is this possible?
Thanks for the help, I greatly appreciate it.
UPDATE PLEASE FIND THE CODE SAMPLES BELOW
Index.php
<?php
include ("config.php");
require_once './vendor/autoload.php';
use Twilio\TwiML\VoiceResponse;
$response = new VoiceResponse();
$twilionumber = ltrim($_POST['To'], '+');
$callernumber=ltrim($_POST['From'], '+');
createCall($phonenumbertouse,$response,$twilionumber);
echo $response;
function createCall($phonenumbertouse,$response,$twilionumber) {
$dial = $response->dial('',['timeout' => '30']);
$dial->number($phonenumbertouse, ['action' => "http://example.com/whisper.php",'method' => 'GET']);
}
WHISPER.PHP
<?php
include ("config.php");
require_once './vendor/autoload.php';
use Twilio\TwiML\VoiceResponse;
$response = new VoiceResponse();
$gather = $response->gather(['action' => "http://example.com/route.php",
'method' => 'GET']);
$gather->say($whisper, ['voice' => 'woman', 'language' => 'en-US']);
echo $response;
?>
route.php
<?php
include ("config.php");
require_once './vendor/autoload.php';
use Twilio\TwiML\VoiceResponse;
use Twilio\Rest\Client;
$response = new VoiceResponse();
$keyedInput=$_REQUEST['Digits'];
$mycallsid=$_REQUEST['ParentCallSid'];
if ($keyedInput == 1){
$response->say('connecting the call');
}
elseif ($keyedInput == 2){
$twilio = new Client($sid, $token);
$call = $twilio->calls($mycallsid)
->update([
"method" => "POST",
"url" => "http://example.com/redirect.php"
]
);
}
elseif ($keyedInput == 3){
$response->say('you selected 3');
}
else {
$response->say('Sorry, I don\'t understand that choice.');
}
echo $response;
?>
**Redirect.php **
<?php
include ("config.php");
require_once './vendor/autoload.php';
use Twilio\TwiML\VoiceResponse;
$response = new VoiceResponse();
$dial = $response->dial('+14151234567',['timeout' => '30']);
echo $response;
?>
Twilio developer evangelist here.
Your issue here is that you are using the wrong CallSid to update.
In this case there are two CallSids at play. When your user dials in to your Twilio number, that call between the user and Twilio has one CallSid. Then, when Twilio creates an outbound call to the operator, that call has a different CallSid.
In your application, when you get the <Gather> response from the operator, the CallSid being sent to your endpoint is the CallSid of the operator's call leg. Instead, you need to find the CallSid of the original call.
You should find that the ParentCallSid parameter is sent to the webhook endpoint as well. That ParentCallSid is the original inbound call's Sid and is what you should use to redirect the caller to another operator.
Edit
OK, so I had a go at building this. I don't normally work in PHP, so I wrote this in Node.js (as Twilio Functions). I got it to work and the answer still seems to me to be "use the ParentCallSid, so hopefully it gives you some idea where you might have gone wrong.
The incoming call
This makes an outbound call to my cell phone number, with a url set to make the whisper to me when I answer the call.
exports.handler = function (context, event, callback) {
const twiml = new Twilio.twiml.VoiceResponse();
const dial = twiml.dial();
dial.number({ url: "/whisper" }, MY_CELL_PHONE_NUMBER);
callback(null, twiml);
};
<Response>
<Dial><Number url="/whisper">MY_CELL_PHONE_NUMBER</Number></Dial>
</Response>
The whisper
This returns a <Gather> spoken to the person answering the cell phone number. It offers a choice, dial "1" to connect or "2" to hang up, triggering the original caller to dial to another number.
exports.handler = function (context, event, callback) {
const twiml = new Twilio.twiml.VoiceResponse();
const gather = twiml.gather({ digits: 1, action: "/after-whisper" });
gather.say("Dial 1 to connect, dial 2 to hang up.");
callback(null, twiml);
};
<Response>
<Gather action="/after-whisper" digits="1">
<Say>Dial 1 to connect, dial 2 to hang up.</Say>
</Gather>
</Response>
The Gather action "/after-whisper"
This checks the Digits parameter. If it is "1" it immediately calls back with an empty response, signalling the end of the whisper and leading the call to connect with the original caller.
If the Digits parameter is not "1" then we initialise a Twilio client and make a request to the API to update the call with the sid ParentCallSid to a new URL. Once that is complete we return TwiML that says to <Hangup> the whisper call (which wasn't strictly necessary, but I like the intention).
exports.handler = async function (context, event, callback) {
const twiml = new Twilio.twiml.VoiceResponse();
if (event.Digits === "1") {
callback(null, twiml);
} else {
const client = context.getTwilioClient();
try {
// Update the parent call with a new URL.
await client.calls(event.ParentCallSid).update({
url: URL_TO_NEXT_TWIML,
});
} catch (error) {
console.log(error);
}
twiml.hangup();
callback(null, twiml);
}
};
The next TwiML
This is the endpoint that the url above relates to, when it gets the callback it just returns a <Dial> to another number, though it could include another whisper and go round this loop again if desired.
exports.handler = async function (context, event, callback) {
const twiml = new Twilio.twiml.VoiceResponse();
twiml.dial(NEXT_NUMBER_TO_DIAL);
callback(null, twiml);
};
<Response>
<Dial>NEXT_NUMBER_TO_DIAL</Dial>
</Response>
I already had this function in my Twilio Functions section, although I'm not sure if I copy-pasted this from somewhere a while ago or if it came by default:
var moment = require('moment-timezone')
exports.handler = function(context, event, callback) {
let now = moment().tz('Australia/Brisbane');
let isWorkday = (now.isoWeekday() < 6);
let isWorkingHour = (now.hour() > 7 && now.hour() < 17);
let response = {};
if(isWorkday && isWorkingHour) {
callback(null, response);
} else {
callback("Service is closed");
}
};
I also found another SO post where someone included a basic function to divert to voicemail if a call is unanswered:
exports.handler = function(context, event, callback) {
const twiml = new Twilio.twiml.VoiceResponse();
if (event.DialCallStatus === 'completed' || event.DialCallStatus === 'answered') {
twiml.hangup();
} else {
twiml.say("Service is closed");
twiml.record({
transcribe: true,
transcribeCallback: "http://twimlets.com/voicemail?Email=example#domain.com",
action: "/hangup"
});
}
callback(null, twiml);
};
What I want to do is basically combine these two so that:
If call is received where !isWorkday || !isWorkingHour then send straight to voicemail. Don't ring the phone at all.
If call is receive where isWorkday && isWorkingHour then run something like this Twiml bin:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial>
<Sip>
me#myuniqueid.sip.us1.twilio.com;region=au1
</Sip>
</Dial>
</Response>
If the call is not answered within 20 seconds then send to voicemail (with a different greeting to step 1).
Bonus question: I obviously also need to be able to listen to the voicemail as I doubt the transcribing will be very accurate. Is there any way to include a link to the voicemail (or the voicemail mp3 itself) in the email I receive when a new voicemail is created? Or can I create a function/twiml bin for outgoing calls that would let me dial a number and listen to my voicemails, like how normal voicemail works??
Heyo, Twilio Developer Evangelist here. 👋
I just set down and built your use case in a single function. There is a lot going on inside of this one function (and I might recommend splitting the work into several functions).
I'm happy to answer your bonus questions, too, but please open it separately to keep this answer focused. :)
Let's have a look at a working example!
const moment = require('moment-timezone');
function isServiceOpen() {
let now = moment().tz('Australia/Brisbane');
let isWorkday = now.isoWeekday() < 6;
let isWorkingHour = now.hour() > 7 && now.hour() < 17;
return isWorkday && isWorkingHour;
}
exports.handler = function(context, event, callback) {
const twiml = new Twilio.twiml.VoiceResponse();
console.log(event);
const callIncludesRecording = !!event.RecordingUrl;
const callWasAnswered = event.DialCallStatus === 'completed';
const callWasNotAnswered = event.DialCallStatus === 'no-answer';
const serviceIsOpen = isServiceOpen();
if (callWasAnswered) {
twiml.hangup();
} else if (callIncludesRecording) {
console.log('Call includes recording!');
// do something with the recording URL here
console.log(event.RecordingUrl);
twiml.say("Thank you! We'll come back to you shortly.");
twiml.hangup();
} else if (callWasNotAnswered) {
console.log('Call was not answered...');
twiml.say(
'Unfortunately no one can answer right now. But you can leave a message.'
);
twiml.record({
action: '/handle-call'
});
} else if (!serviceIsOpen) {
console.log('Service is closed...');
twiml.say('Service is closed but you can leave a message');
twiml.record({
action: '/handle-call'
});
} else {
twiml.dial(
{
action: '/handle-call',
method: 'POST',
timeout: 5
},
'+4915...'
);
}
callback(null, twiml);
};
The function you see above is available under a /handle-call endpoint and answers all webhooks for a call.
Scenario 1 - a call is not answered
At the end of the function, you see the dial function call. The important piece for this case is that dial supports a timeout and an action attribute.
twiml.dial(
{
action: '/handle-call',
method: 'POST',
timeout: 30
},
'+49157...'
);
The above tells Twilio to try to call the number +49157... for 30 seconds (it's actually closer to 35 – you can read details in the docs). If the call ends or no one answered the call until the timeout is reached Twilio will ask the defined action URL for additional TwiML configuration.
The URL in the action attribute references the same function path (/handle-call) and the same function will be executed again but this time the event object will include a DialCallStatus of no-answer (have a look at the variable callWasNotAnswered). If the call was not answered you can return TwiML to say a message and tell the API to start the recording of the call.
// no one answered – let's record
twiml.say(
'Unfortunately, no one can answer right now. But you can leave a message.'
);
twiml.record({
action: '/handle-call'
});
The record verb also allows an action attribute which lets you define a URL that should be requested when the recording finished (we'll use again the same function under the same /handle-call endpoint).
The call to the same URL will then include a RecordingUrl inside of the event object. If this property is present you know that it is the result of the recording. At this stage, it is time to do something with the recording URL (send a message, log it, ...), to finish the call after saying "goodbye" and to hang up.
// do something with the recording URL here
console.log(event.RecordingUrl);
twiml.say("Thank you! We'll come back to you shortly.");
twiml.hangup();
The webhook flow is as follows:
POST /handle-call (initial webhook) -> dial +49157...
POST /handle-call (call after time out) -> say "unfortunately, ..." & record
POST /handle-call (recording finished) -> say "thank you" & hangup
Scenario 2 - the call is outside of business hours
For this scenario, I took the logic that you already provided and created a isServiceOpen helper function. When a call comes in outside of business hours the function responds with TwiML defining a message and a recording.
twiml.say('Service is closed but you can leave a message');
twiml.record({
action: '/handle-call'
});
After the finished recording the our function will be called (/handle-call). This time, the request will include a RecordingUrl and the handling will be the same as in scenario 1 (log the recording URL and hangup).
The webhook flow is as follows:
POST /handle-call (initial webhook) -> say "service is closed" & record
POST /handle-call (recording finished) -> say "thank you" & hangup
Scenario 3 – the call was answered
In case the call happens in business hours and was answered quickly enough there won't be a need for a recording. Because the dial verb includes an action attribute (we used this in scenario 1) another webhook will be sent after the call is ended.
This webhook will include a DialCallStatus parameter with the completed value. Then it's time to end the call and hang up.
twiml.hangup();
POST /handle-call (initial webhook) -> dial "+49157..."
POST /handle-call (call finished) -> hangup
I hope the above helps. 😊 As mentioned initially, it might be a good idea to split the functionality into /handle-call, /handle-recording and other functions.
Let me know if that helps and you have any further questions. :)
I'm trying to set up voicemail for a Twilio # (Not Twilio Cell)... And it works, for the most part, except I get called back 20 seconds later with nothing there (Step 5 Below). I think my issues are in TWIML 1/2 from testing, but I don't know.
I'll call in to my Twilo # from my cell.
I hit the reject on my cell (Even though my time out is 1, it doesn't give up.).
Leave the message
Hang up || press button to end
[~20 seconds after step 4] Get blank call back
[10-15 secs after step 5] Get my text / email as expected
Am I doing something wrong in TWIML 2?
or
Is using my cell for orig/term causing problems?
Incoming:
TWIML 1:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial action="https://-->TWIML 2"
timeout="1"
callerId="{{To}}"
>
<Number>##CELL##</Number>
</Dial>
</Response>
TWIML 2:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>
Please leave a message
</Say>
<Record action="https://TWIML 3"
method="POST"
maxLength="120"
playBeep="false"
trim="do-not-trim"
transcribe="true"
transcribeCallback="https://FUNCTION 1"
/>
<Say>I did not receive a recording.</Say>
</Response>
TWIML 3
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>Bi</Say>
</Response>
FUNCTION 1
exports.handler = function(context, event, callback) {
var client = context.getTwilioClient();
const mailgun = require('mailgun-js')({apiKey: context.MG_KEY, domain:context.MG_DOMAIN});
var async = require('async');
var sendSMS = function(callback){
let dataSMS = {
to: '##CELL##',
from: event.To,
body: 'Voicemail from:\n ' + event.From + '\n\nText:\n' + event.TranscriptionText + '\n\n---MSG Recording---\n' + event.RecordingUrl
};
client.messages.create(dataSMS).then(function(response){
console.log('N [T1]: ' + response.status);
return callback(null, response);
}).catch(function(err){
console.log('T2: ' + err);
});
};
var sendEmail = function(callback){
let dataMG = {
to: '##TO##',
from: '##FROM##',
subject: 'Voicemail: ' + event.From,
text: event.TranscriptionText + '\n\n--- MSG Recording --->\n' + event.RecordingUrl
};
console.log('-------- Sending Mail (Mailgun API)---->' + dataMG.to);
mailgun.messages().send(dataMG, function (error, body) {
console.log('N [MB] Mailgun Body :' + body);
return callback(null, body);
});
};
var waitTime = function(callback){
setTimeout(function() {
return callback(200);
}, 1000);
};
var asyncTasks = [sendSMS, sendEmail, waitTime];
async.parallel(asyncTasks, function(err, result){
console.log('--------------------------DONE--------------------------' + result[0] + result[1]);
callback(null, 200);
});
};
Resolved:
So a couple days later and it is working as expected, without being phoned back ~20 seconds later. I don't think I changed anything, but who knows; now I'm having trouble forcing the unexpected behavior to occur :)
Below is an example of how to set up a voicemail system that will transcribe your message to a MMS including a link to the original voice message. This also includes an email copy, if you had a mailgun account set up.
There is an extra fee associated with using the transcription service, and you will have to delete the recordings eventually after you no longer want access to the recording.
If you are to try this out, when clean the code for you situation, the timeout variable in Twiml 1 is how many seconds it will ring for. Function 1 requires async, but you could write it without when tailoring for you situation.
I created TwiML application and set Voice request URL the web deployed ASP.NET-MVC aplication action method: http://voiceapp-001-site1.myasp.net/voice
This action method is invoked when somebody goes to the URL posted above:
public ActionResult Voice() {
Response.ContentType = "text/xml";
// put a phone number you've verified with Twilio to use as a caller ID number
string callerId = Settings.TwilioNumber;
// put your default Twilio Client name here, for when a phone number isn't given
string number = "jenny";
// get the phone number from the page request parameters, if given
if (Request["PhoneNumber"] != null) {
number = Request["PhoneNumber"];
}
// wrap the phone number or client name in the appropriate TwiML verb
// by checking if the number given has only digits and format symbols
string numberOrClient;
var m = Regex.Match(number, #"^[\d\+\-\(\) ]+$");
if (m.Success) {
numberOrClient = string.Format("<Number>{0}</Number>", number);
} else {
numberOrClient = string.Format("<Client>{0}</Client>", number);
}
ViewBag.CallerId = callerId;
ViewBag.NumberOfClient = numberOrClient;
return View();
}
The Voice view looks like:
<?xml version="1.0" encoding="UTF-8" ?>
<response>
<dial callerid="#ViewBag.CallerId">
#Html.Raw(ViewBag.NumberOfClient)
</dial>
</response>
Then I try to make test call:
but after 13 seconds call is automatically terminated and In the error log I get:
Notification SID NOe85ffe80dfc52e81f942a7414c9f7c9c
Warning 12200 Schema validation warning
Description Cannot find the declaration of element 'response'.
But below in the Body section I can see the element response:
Hi Twilio developer evangelist here.
Can you try modifying your view to look like this instead?
<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<Dial callerId="#ViewBag.CallerId">
#Html.Raw(ViewBag.NumberOfClient)
</Dial>
</Response>
Remember XML is case-sensitive, so the casing in your XML tags actually matter. Also, I would suggest using the Twilio.TwiML helper library. With that, you can get your XML generated for you with the right casing, and avoiding typos altogether.
Here's how you'd do it:
var twiml = new TwilioResponse();
var dialAttributes = new {callerId = "48326304351"};
var dial = twiml.Dial("+15555555555", dialAttributes);
return TwiML(dial);
**
<Response>
<Dial record="true" timeout="15" timeLimit="4257" callerId="+14589775871" action="http://demo.com/CallCharge.php?rid=81;4260" >
<Number url="http://demo.com/CallReceiver.php?name=Deval">+14582783238 </Number>
</Dial>
</Response>
**
In the above twiml,
*I call to 14582783238 number from this twilio number 14589775871
Receiver (14582783238) decline the call but it still it connected the called by 14589775871*
As per twilio rule
1) if receiver pickup the Call than it will go to this url "http://demo.com/CallReceiver.php?name=Deval" say message
2) if receiver decline the Call than it will not got to this url "http://demo.com/CallReceiver.php?name=Deval" but it will happen over their.
In second point, Twilio call not properly handle it or i doing somthing wrong here?
Please let me know as soon as possible about this matter.
http://demo.com/CallCharge.php is called at the end of the call, be it after a call or on hang-up.
Twilio will automatically pass the DialCallStatus, DialCallSid, DialCallDuration and RecordingUrl request parameters to your action URL.
<?php
/* CallCharge.php */
$DialCallStatus = isset($_REQUEST['DialCallStatus']) ? $_REQUEST['DialCallStatus'] : "";
$DialCallDuration = isset($_REQUEST['DialCallDuration']) ? $_REQUEST['DialCallDuration'] : "";
if($DialCallStatus != "completed") {
// Don't charge
} else {
// Charge $DialCallDuration
}
https://www.twilio.com/docs/api/twiml/dial