Make a serverless phone proxy in Twilio

Completed Code Here

Communication in the current world is complicated. We want to be connected to everyone, but at the same time, we want boundaries and space. We don’t want to juggle 7 different phones for work, relationships, and personal friends, but we want a central place to be able to communicate with them all. It would be nice if we could have a proxy phone number, allowing people to contact us without having our personal number.

That’s exactly what we will be doing in this post. We are going to make a phone number and voicemail proxy that allows us to receive calls and can save voicemails for us. If the owner calls, it will let you listen to voicemails, and if anyone else calls, it will forward the call or let the caller leave a voicemail.

We will be developing and testing the application locally before launching this as a serverless application on twilio’s cloud.

Pre-reqs
Twilio account
Twilio number
ngrok account
NodeJS (I recommend using NVM to manage Node Versions)

Configure your environment

First, we want to make sure that you have NodeJS v14 installed. We will install nvm and use it to set version 14 manually as it is not the most recent version of NodeJS.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

After installing NVM, restart your console and then run

nvm install 14
nvm use 14

Now we need to install and configure the Twilio Cli. You’ll need to sign up for a Twilio account if you haven’t already. It requires putting in your account id and secret found on the twilio dashboard.

npm install twilio-cli -g
twilio login
twilio plugins:install @twilio-labs/plugin-serverless

If this is your first time using the Twilio CLI and you don’t have an active profile set yet, run twilio profiles:use PROFILE_ID once before trying any of the following serverless commands.

Create the service

Let’s generate a new serverless twilio project called voice-proxy by running twilio serverless:init –empty voice-proxy.

CD into the generated folder and add the following to your .env file

ACCOUNT_SID=(autofilled from generation)
AUTH_TOKEN=//get from twilio website console
MY_PHONE_NUMBER='+1xxxxxxxxxx' (Your phone number)
TWILIO_PHONE_NUMBER='+1xxxxxxxxxx'

Write the proxy

Let’s start writing our functions. All of the functions we create will go into the functions folder in our project.

Our main file will check who is calling. If the owner of the voicemail is calling, it will forward them to their voicemail. Otherwise, if a regular person is calling, it will transfer them to the call handling function.

/**
 * Returns TwiML that prompts the users to make a choice.
 * If the user enters something it will trigger the handle-user-input Function and otherwise go in a loop.
 */
 exports.handler = function (context, event, callback) {
    const OWNERPHONE = context.MY_PHONE_NUMBER
    
    const twiml = new Twilio.twiml.VoiceResponse();
  
    // When the owner of the voicemail calls in, we transfer
    // them right to the voicemail
    if (event.From === OWNERPHONE)  {
      twiml.redirect("./voicemail-loop?index=0")
    } else {
      // We connect the unknown caller to us with twiml.dial
      // if the call completes or times out we move to the handler in the action option
      twiml.dial({
        action: 'handle-call',
        timeout: 17
      }, OWNERPHONE)
    }
    
    callback(null, twiml);
  };

Code voicemail loop

First, we will make an api call to pull down the voicemails connected to our account and play them.

…
const VOICEMAIL_NUMBER = context.TWILIO_PHONE_NUMBER;

// Get list of voicemails. Sort by newest first
  const voicemailClean = [];
  let voicemailList = await client.recordings.list({to: VOICEMAIL_NUMBER});
  // split string, add mp3 at the end, loop through playing them
  voicemailList.forEach(function (c) {
    let audioUri = `https://api.twilio.com${c.uri.slice(0, -4)}mp3`;
    voicemailClean.push(audioUri);
  });

voicemailClean.forEach(item => twiml.play(item))

We want to add gather commands so that we can delete or replay messages. In order to integrate gather commands, we need to be able to redirect back to this function after each option select. To make this work, we’ll make this function be able to call back to itself and keep track of the index of which voicemail we are on by carrying the index over.

exports.handler = async function(context, event, callback) {
  // Here's an example of setting up some TWiML to respond to with this function
	let twiml = new Twilio.twiml.VoiceResponse();
  // Initiate a Twilio API client. Make sure you enabled exposing username and password
  const client = context.getTwilioClient();

  let Index = parseInt(event.index) || 0;
  let UserInput = event.Digits || event.SpeechResult || '1';
  const VOICEMAIL_NUMBER = context.TWILIO_PHONE_NUMBER;


  // Get list of voicemails. Sort by newest first
  const voicemailClean = [];
  let voicemailList = await client.recordings.list({to: VOICEMAIL_NUMBER});
  // split string, add mp3 at the end, loop through playing them
  voicemailList.forEach(function (c) {
    let audioUri = `https://api.twilio.com${c.uri.slice(0, -4)}mp3`;
    voicemailClean.push(audioUri);
  });


  if (UserInput.length > 1) {
    if (UserInput.toLowerCase().includes('next')) {
      UserInput = '1';
    } else if (UserInput.toLowerCase().includes('replay')) {
      UserInput = '2';
    } else if (UserInput.toLowerCase().includes('delete')) {
      UserInput = '3';
    } else if (UserInput.toLowerCase().includes('restart')) {
      UserInput = '9';
    }
  }

  switch (UserInput) {
    case '1':
      // Do nothing
      break;
    case '2':
      Index = Index - 1;
      break;
    case '3':
      let deleteResponse = await client.recordings(voicemailList[Index - 1].sid).remove();
      voicemailList.splice(Index - 1, 1);
      voicemailClean.splice(Index - 1, 1)
      Index = Index - 1;
      twiml.say('Message deleted');
      break;
    case '9':
      Index = 0
      break;
    default:
      twiml.say('We are sorry, we did not recognize your option. Please try again.');
      twiml.redirect('voicemail-loop');
  }


  // Someone accidentally hit 1 on the last voicemail
  if (Index === voicemailList.length) {
    Index = 0;
  }

  if (voicemailList.length === 0) {
    twiml.say('You have no messages. Goodbye');
    twiml.hangup();
  }

  if (Index === 0) {
    twiml.say(`You have ${voicemailList.length} messages`);
  }


  twiml.say(`Message ${Index + 1} of ${voicemailList.length}`);
  twiml.play(voicemailClean[Index]);

  const gather = twiml.gather({
    numDigits: 1,
    action: `https://voice-ivr1-3843-cmbffn.twil.io/voicemail-loop?index=${Index + 1}`,
    hints: 'next, replay, delete, restart',
    input: 'speech dtmf',
  });


  if (Index + 1 !== voicemailList.length) {
    gather.say('Press 1 or say next to play the next message');
  }
  gather.say('Press 2 or say replay to replay this message');
  gather.say('Press 3 or say delete to delete this message');
  if (Index + 1 === voicemailList.length) {
    gather.say('To hear your voicemails again, press 9 or say restart');
  }


  // This callback is what is returned in response to this function being invoked.
  // It's really important! E.g. you might respond with TWiML here for a voice or SMS response.
  // Or you might return JSON data to a studio flow. Don't forget it!
  return callback(null, twiml);
};

handle call

Now that the voicemail reading is handled, let’s handle when a caller leaves a voicemail.

 exports.handler = function (context, event, callback) {
    
    const twiml = new Twilio.twiml.VoiceResponse();
    // The call timed out or didn't complete. We will take a voicemail.
    // Otherwise, the call completed and we exit
    if (event.DialCallStatus === 'no-answer' || event.DialCallStatus === 'failed' || event.DialCallStatus === 'busy' || event.DialCallStatus === 'canceled') {
        twiml.say('Please record your message after the tone. Press 1 when youre done recording');
        twiml.record({
          transcribe: true,
          timeout: 10,
          finishOnKey: '1',
          action: 'voicemail-complete',
          maxLength: 30
        });
    }
    
    callback(null, twiml);
  };

voicemail-complete.js

We need a handler for when the voicemail is recorded, so let’s make it here.

exports.handler = function(context, event, callback) {
    // Here's an example of setting up some TWiML to respond to with this function
    let twiml = new Twilio.twiml.VoiceResponse();
    twiml.say('Your message has been saved. Goodbye!');
    twiml.hangup();
    return callback(null, twiml);
  };

Test functions locally

Now, let’s run our functions locally and test them. Run

twilio serverless:start

In the root directory of the project. If you want to run it in the background and use the same tab, run

twilio serverless:start &

Open a second terminal tab and configure ngrok. Log into your ngrok account and run this to add your credentials. twilio-run uses v2 of ngrok. You can download v2 of ngrok from here - https://dl.equinox.io/ngrok/ngrok/stable/archive

ngrok authtoken xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Or, you can manually create an ngrok.yml in the following directory.

/home/<YourUsername>/.ngrok2/ngrok.yml

authtoken: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Now that you’re logged in, run this command to connect your twilio number to your local function.

twilio phone-numbers:update "+1xxxxxxxxxx" --voice-url http://localhost:3000/voicemail-proxy

Now you can call your twilio number and execute your function.

Test call

Ask a friend to call your phone number. Have them leave a voicemail. Now try calling from your phone number and listen to the voicemail. Pretty cool, huh?

Deploy

Now that you know it works, let’s deploy it to twilio’s network. Run

twilio serverless:deploy

To deploy your project to the serverless network. The output should give you back a url to your functions like this:

✔ Serverless project successfully deployed
……..
Functions:
   https://proxy-test-1300-dev.twil.io/handle-call
   https://proxy-test-1300-dev.twil.io/voicemail-complete
   https://proxy-test-1300-dev.twil.io/voicemail-loop
   https://proxy-test-1300-dev.twil.io/voicemail-proxy
Assets:

Copy the output to your voicemail-proxy url and connect it to your twilio number.

twilio phone-numbers:update "+1xxxxxxxxxx" --voice-url http://your-url/voicemail-proxy

Now try calling the number and testing it. Congratulations! Your project works and is deployed serverlessly.

Conclusion

A voicemail proxy like this can be used for multiple reasons. It can allow you to have a phone number for each area of your life, like a number for managing business calls, a number for meeting new people, etc, all without giving out your personal phone number.