Build an IVR with Voicemails Forwarded to Email with Node.JS

This advanced example builds an application that implements a simple phone tree IVR with a few interesting features, including:

  • Parallel dialing to multiple phone numbers
  • Recording a voicemail and using a dynamic greeting
  • Transcribing voicemail and sending both the text and the recording via email

What do you need to run this code?

The application also uses Mailgun to send the emails, and you will need an API key from that service. You could also use any other email API.

You will also need the SignalWire Node.JS SDK.

Configuring the Code

Start by copying the env.example file to a file named .env, and fill in all of the environment variables with the correct values for your IVR & email settings.

The core of the application is the entry route, where we ask for the user's choice via DTMF."/entry", (req, res, next) => {
  var response = new RestClient.LaML.VoiceResponse();
  gather = response.gather({ timeout: 5, numDigits: 1, action: formatUrl('mainmenu') })
  gather.say("Hello! Press 1 for sales, 2 for recruiting or 4 for accounting.")

  respondAndLog(res, response);

The SignalWire Compatibility APIs require that code is translated into XML, so here is the XML version of what the code above does!

<?xml version="1.0" encoding="UTF-8"?>
 <Gather timeout="5" numDigits="1" action="/mainmenu">
   <Say>Hello! Press 1 for sales, 2 for recruiting or 4 for accounting.</Say>

Based on what number the user inputs, we then redirect to the correct action in the mainmenu handler. Whether the user dials sales, recruiting, or accounting, you can see below that we dial every member of the group at the same time. When one person picks up, the rest of the calls will be terminated."/mainmenu", (req, res, next) => {
  var response = new RestClient.LaML.VoiceResponse();

  switch (req.body.Digits) {
    case "2":
      dial = response.dial({timeout: 20, action: formatUrl('voicemail', "?Email=" + JOBS_EMAIL + "&Message=Recruiting")});
      var recruiters = RECRUITERS_GROUP.split(',')
      recruiters.forEach(function(item) {
    case "4":
      dial = response.dial({timeout: 20, action: formatUrl('voicemail', "?Email=" + ACCOUNT_EMAIL + "&Message=Accounting")});
      dial = response.dial({timeout: 20, action: formatUrl('primarysalesdial')});
  respondAndLog(res, response);

For example, if the caller enters 2, this is the XML that is returned. Multiple <Number> entries are dialed at the same time, the first one to pick up hangs up the other.

<?xml version="1.0" encoding="UTF-8"?>
  <Dial timeout="20" action="/voic[email protected]&amp;Message=Recruiting">

What is very important here is the action parameter in each dial. The voicemail URL is what will be triggered when a call ends, and we will need to check if the call had actually connected to the intended department or not. If not, we will send them to voicemail. Note that we also pass the department name via query string so the greeting is dynamic."/voicemail", (req, res, next) => {
  var response = new RestClient.LaML.VoiceResponse();
  if (req.body.DialCallStatus != "completed") {
    // it means the call was not answered
    response.say("Our " + req.query.Message + " department is currently unavailable. Please leave a message after the beep.")
    action = formatUrl('voicemailhandler', "?Email=" + req.query.Email)
    response.record({transcribe: true, transcribeCallback: action, action: action })
  respondAndLog(res, response);

generating the following XML:

<?xml version="1.0" encoding="UTF-8"?>
 <Say>Our Accounting department is currently unavailable. Please leave a message after the beep.</Say>
 <Record transcribe="true" transcribeCallback="/[email protected]" action="/[email protected]"/>

In the above, we set a transcribeCallback for the <Record> action. This allows our callback to receive both the recording file URL and the transcription text.

Handling the transcription is made a bit tricky by the fact that it is asynchronous, so the handler gets called twice, once for the recording URL and once for the transcription. We get around that by temporarily saving the recording in a hash in memory. In a production environment, this should be replaced by a database.

When the second request comes with the transcription, we use Mailgun to send an email containing the recording file and the recording transcription."/voicemailhandler", (req, res, next) => {
 if (req.body.TranscriptionText) {
   console.log("Got transcription, send email " + req.query.Email + RECORDING_DB[req.body.CallSid] + req.body.TranscriptionText);
   emailTranscription(req.query.Email, RECORDING_DB[req.body.CallSid], req.body.TranscriptionText, req.body.From);
   // avoid leaking memomry
   delete RECORDING_DB[req.body.CallSid]; 
 } else if (req.body.RecordingUrl) {
   console.log('stash recording for later');
   RECORDING_DB[req.body.CallSid] = req.body.RecordingUrl

 var response = new RestClient.LaML.VoiceResponse();
 respondAndLog(res, response);

Running the application

If you are running the application locally, first load the .env file with set -o allexport; source .env; set +o allexport, then run npm install followed by npm start.

It is simpler to run the application via Docker, by first building the image with docker build -t nodeivr . followed by docker run -it --rm -p 3000:3000 --name nodeivr --env-file .env nodeivr.

Either way, after starting the application head to http://localhost:3000 to see a test page.

You may need to use a SSH tunnel for testing this code if running on your local machine. – we recommend ngrok. You can learn more about how to use ngrok here.

When you configure a SignalWire DID to test, you should add /entry to the end of the URL you input, such as

Sign Up Here

If you would like to test this example out, you can create a SignalWire account and space here.

Please feel free to reach out to us on our Community Slack or create a Support ticket if you need guidance!

What’s Next

Check out the full code on our SignalWire Github Repo!

Did this page help you?