Building a work phone with Node.js, Apex, Twilio & AWS

Austin Miller
PagerTree
Published in
14 min readNov 7, 2017

Update Oct 22, 2018: A simpler version of this blog post is available here.

Hi there! My name is Austin Miller, I’m the founder of PagerTree. In this post, I will walk you through coding up a work phone for your business using Node.js, Apex, Twilio, AWS.

I am going to make a couple assumptions:

  • You are comfortable writing code in JavaScript
  • You have an AWS & Twilio account setup
  • You have proper access to AWS Lambda, AWS Cloudwatch, AWS API Gateway, & AWS SNS.
  • You have already purchased a phone number from Twilio with voice capabilities.

To follow along, clone the GitHub repo.

So lets get to it!

Requirements

Any good peice of software should start out with some requirements. So this is what I want my work phone to do.

  • During business hours, forward any phone calls coming into the business number to my personal phone number. If I don’t answer, send the caller to voicemail.
  • During non-business hours, tell the caller we are currently closed and then send them to voicemail.
  • When I get a voicemail, send me a notification of some sort. (Bonus points 🌈)

2 basic requirements and some bonus magic; that should be easy! Obviously we could make more, like dialing extensions, but we’ll keep this simple for our first post.

Apex and AWS Lambda — Serverless Awesomeness

So there are tons of things that I love about this section.

  1. Apex — Super awesome management of AWS Lambda functions created by TJ Holowaychuck, possibly the easiest to use. You’ll want to download the right version for you platform: See the docs. (Make to to follow the installation and AWS credentials sections)
  2. AWS Lambda — This is the AWS offering for Serverless technology. What I love about Lambda is their pricing, the first 1 million requests per month are free!

Ok, so lets go ahead and set up our project.

  • Make a folder for your project, I’ll called mine office-phone
  • From the terminal, change directories to your project folder cd office-phone
  • If your like me (on Windows), copy the executable into the project folder. If your on Linux or Mac you can skip this step. From this step forward I will just refer to the apex command as apex, on windows you will have to run .\apex.exe
  • Initialize the Apex project by running apex init
  • This will start to ask you some questions about your project.
Run apex init and answer questions
  • Awesome, now we have a working apex project. Now lets deploy it by running apex deploy
Run apex deploy
  • If you’ve set up all the proper permissions you should now be able to run apex invoke hello
Run apex invoke hello

Again, if everything was set up correctly, this will return the json {"hello":"world"}. If you want to know where that came from see the file .\functions\hello\index.js

Ok so at this point we’ve setup our basic apex project and are ready to start coding.

Twilio — Interfacing the real world

I can’t say enough about how awesome Twilio is. Not only do they have a great API, their reliable and cost effective. Did I mention PagerTree uses Twilio for for its SMS and Voice communications? 😉

Ok, enough bragging, lets get our feet wet and code some.

Having worked through this project before I wrote the post, let me save us some time and outline what we’ll need.

3 functions

  • main — This will be where our initial phone call comes in and will decide about business hours and where to forward calls.
  • record — Responsible for recording when a phone call is missed during business hours, and hanging up the call after a recording.
  • recordstatus — Responsible for sending me a notification when a new voicemail comes in
Our project thus far with 3 functions

Ok, so if your new to Node.js you might be wondering why each function has a package.json . Its because each of these are NPM project. In short, its a package manager for some libraries we are going to import.

For all the projects combined we are going to use several libraries, here they are listed:

If you pulled the example code from the repo, you should just be able to go into every function directory from the command line and run npm install . That will download and install the necessary dependencies for each function.

/project.json

Ok, so I ❤️ environment variables, and our Apex project lets us set environment variables. Inside your project.json you can set variables like so

"environment": {
"COMPANYNAME": "PagerTree",
"NUMBER": "+15555555555",
"RECORDURL": "/prod/record",
"RECORDINGSTATUSURL": "/prod/recordstatus",
"SNSARN": "arn:aws:sns:<region>:000000000000:office-phone"
}

and access them inside your code via process.env.COMPANYNAME. For this project I have included an example_project.json with the minimum defaults you’ll need. I’ve also included a example_full_project.json with all the possible environment variables that you can use for the project.

Function: main

Ok, there’s quite a bit of code here, but most of it is just getting environment variables and assigning sensible defaults.

The real magic happens on line 30. It says,

  • If there is a number to forward to, and this is a business day, and these are business hours, forward this call to my number.
  • If not, tell the caller we’re closed and about our hours. Then let them record or message.

If you copy and paste that code to your main\index.js file and then runapex deploy main; apex invoke main you should get a response that looks like this (depending on what time you run this).

Invoke our new main function

Pretty cool, while we are here coding, lets just go ahead and fill out our other functions.

Function: record

In our main function above, you’ll notice on line 38 we tell Twilio to call another URL when the recording is done or has timed out. That URL, will be this function record. Twilio is going to send us some information in the request, particularly what we care about is the request.body.DialCallStatus . See the docs.

Function: recordstatus (Bonus points 🌈)

In the record function above, on line 27, we provide Twilio with a recording status callback. The recording status callback URL will be called when a recording is finished processing and it will provide the URL of the MP3 file of the recording.

From our requirements we wanted to be notified when a voicemail comes in. We can do that by publishing a message to an AWS SNS topic. By publishing to an SNS topic we can get notified on several channels including Email and SMS.

So at this point, we’ve coded up all of our functions. Lets deploy them all by running the command apex deploy.

Deploy all of our functions via apex deploy

Tying it all together

Ok, so all of these function are great but they won’t work by themselves. We need a little bit of glue to tie them all together.

If we look at our Twilio dashboard, specifically under a phone number we’ve purchased (Home/ Phone Numbers / Manage Numbers / Active Numbers / Configure) we’ll notice a spot for a Webhook when a call comes in.

So in order to make our Lambda Functions work with Twilio, we’ll need to connect them. We’ll do this using API Gateway.

Twilio wants a webhook for incoming calls

API Gateway

API Gateway is pretty cool in the fact that it gives you a highly available API with a couple of clicks. It also scales seamlessly and handles other stuff like security with ease.

Go ahead and open up your API Gateway console and click the + Create API button.

Create an API

We’ll select the option for New API and give our API a name. I called mine office-phone. Then click the Create API button.

Name your new API

That will create us a new API. We’ll now need to add some methods. If your familiar with HTTP talk, these are method handlers for HTTP verbs. For this project we will only be dealing with POST requests.

So lets go ahead and create our first method. We want to have the root “/” path selected, and under actions, click Create Method. There should now be a drop down asking you to select a verb, select POST.

Under the actions button, click create method

Now that we have our first method created, lets hook it up to our Lambda function. Do that by selecting the following options:

  • Integration Type — Lambda Function
  • Use Lambda Proxy Integration — Unchecked
  • Lambda Region — Select whatever region you prefer
  • Lambda Function — office-phone_main

Then click the Save button. It should prompt you with a box titled “Add Permission to lambda function”. Go ahead and click OK.

We now need to configure some small items for this to work. Specifically in 3 parts: Integration Request, Integration Response, and Method Response.

Method execution sections

Integration Request

Click the Integration Request Box. Now we want to modify the Body Mapping Templates.

Make sure that the Request body passthrough radio selected is When no template matches the request Content-Type header

We now want to click Add mapping template with type application/x-www-form-urlencoded.

A popup box will ask to change passthrough behavior, click No, use current settings.

In the template box that pops up below use the following code as the template:

{
"reqbody":"$input.path('$')"
}
Change the Integration Request Body Mapping Template

Click the Save button.

Integration Response

Similarly for the Integration Response we want to change the Body Mapping Template to text/xml.

In the template box use the following code as the template:

#set($inputRoot = $input.path('$'))
$inputRoot.body
Change the Integration Response Body Mapping Template

Then click Save.

Method Response

Lastly we want to change our Method Response to return text/xml; charset=utf-8.

Go ahead and just modify the current Response Body for 200 to Content type text/xml; charset=utf-8.

Then click Add Response. Set the HTTP status code to 500.

Click Add Response Model and set the Content type to text/xml; charset=utf-8 and the Models to Empty.

Change the Method Response Content Type

CORS

Last thing we want to do is enable CORS for this method. We can do this by simply clicking the Enable CORS button, under the actions tab.

Click the Enable CORS button

Leave all the default settings, and click the Enable CORS and replace existing CORS headers button.

Leave default settings and enable CORS

Click Yes, replace existing values to the Confirm Method changes box.

Test It!

We’re almost done here. Now you can test your method by clicking the Test button. (Make sure you are on the POST method)

Click the test button

You won’t need a request body, so just click the Test button.

And if all is configured correctly you should see Response Body that is similar to what we tested earlier using apex invoke main.

The test response

If you’ve gotten this far you are doing great! If you hit some snags, make sure to re-read and make sure you didn’t miss anything. There’s a lot in this API Gateway section that could get missed.

Create the REST (pun intended 😆) of the Endpoints

So we need two more endpoints for this project to work, namely /record and /recordstatus. This is similar to what we did above.

First start by clicking Create Resource, under the actions button.

Click the Create Resource button

For the resource name you’ll put record. Go ahead and check the Enable API Gateway CORS to save yourself some time. Then click Click Resource.

Create a new resource

⭐️Now do the same steps we did before, by creating POST methods for your new resource. Mapping them to our Lambda functions. Make sure to change the Integration Request, Integration Response, and Method Response! ⭐️

The /record method should map to the office-phone_record Lambda function.

The /recordstatus method should map to the office-phone_recordstatus Lambda function.

Once you have that all set up your API should look like this:

Final API Gateway Resources

Ship It!

We’re finally at the last step in our API Gateway. Whew! Lets ship it.

Under the actions button, click Deploy API.

Click Deploy API

A popup box will ask you what stage to deploy this to. We want to say [New Stage], and we’ll name it prod. Then click Deploy.

Create new stage prod and deploy

If all went well you should now see a URL for your API gateway

API Gateway URL

Finally, copy that URL to your clipboard. We’ll need it for the next step.

Configure Twilio

The last step is to configure our Twilio number to use our newly created API.

In the Twilio Console

Go ahead and paste the URL you copied earlier in the box under voice, that says A Call Comes In. Make sure the method is HTTP POST.

Once, you’ve done that click the Save button.

Kick the Tires!

Wow! That’s been a lot of work! But now give your Twilio business phone number a call and see how it works! Make sure you don’t call from the phone that the call will be forwarded to 😉

If you ignore the call, it will send them to voicemail and prompt for a message. You can then see these recordings in your Twilio dashboard here.

That’s pretty awesome, but frankly I don’t want to monitor my Twilio dashboard all day. I want to get notified when someone leaves a message.

Get notified on a new Voicemail (Bonus points 🌈)

I didn’t touch on this too much earlier, and if you looked at the code of our recordstatus function, it might have looked like a waste of time, but this might be the most powerful function in the project. It sends a message to AWS SNS saying we got a voicemail.

If you don’t know how SNS works, I highly recommend reading the getting started guide. In short, with SNS you can fan-out messages to a large number of subscribers. This will be important because we want to get alerted by SMS and Email that we got a new voicemail.

In the AWS SNS Console

In the AWS console create a new topic. We can go ahead and call it office-phone. You’ll also need a display name so we can get SMS notifications. Its limit is 10 characters so I used officephon.

Click the Create topic button.

Create an SNS Topic

The next thing we want to do is create a subscription. So click the Create subscription button.

Lets first make the SMS subscription. So for the protocol select SMS and for the endpoint enter in your phone number. Click Create subscription.

Create SMS subscription

Next we’ll make the Email subscription. So for the protocol select Email and for the endpoint enter in your email. Click Create subscription.

You should then get an email that asks you to confirm the subscription. Click the Confirm subscription link in the email.

If you then click the Refresh button, you’ll notice we now have two subscriptions.

Our SNS subscriptions

If you were to publish a message to this topic you would get a notification to both your phone and your email. However, currently only you have the ability to publish to this topic. We need our lambda function to be able to publish to this topic. In order to to that we will have to add some permissions for the AWS Lambda role.

So from this page (SNS Topic: office-phone) copy the Topic ARN.

In the AWS IAM Console

In the AWS IAM console we’ll need to do two things:

  1. Create a policy that allows access to publish to our SNS topic
  2. Attach the policy to the office-phone_lambda_function role

Create the IAM Policy

Click the Create policy button

Click Create policy

Select Create Your Own Policy

Select Create Your Own Policy

Give your policy a name (I used sns-office-phone-publish). Give it a good description, the copy and paste the following code for the Policy Document. Replace <SNSARN> with the ARN of our office-phone topic that you copied earlier.

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sns:Publish"
],
"Resource": [
"<SNSARN>"
]
}
]
}

It should look like this:

Then click the Create Policy button.

Attach the Policy to the Lambda Role

Now we need to attach this policy to the role that runs our Lambda functions. If you quickly look in /project.json you’ll notice a configuration setting named role. It should look similar to this:

"role": "arn:aws:iam::000000000000:role/office-phone_lambda_function",

That’s the role that runs our functions. We need to attach the policy here. So let’s go ahead and attach our newly created policy to the role.

In the Roles tab, in the IAM console, click the office-phone_lambda_function role.

From the Role page, click the Attach Policy button.

Click attach policy on the office-phone_lambda_function role

Now in the search bar, type “office-phone”. You should see the policy we recently created.

Check the box next to the policy, and the click Attach policy button.

Attach the policy to the role

Congrats! Your done! 👏

If you reading this, give your self a high five for a job well done. You now have a pretty decent office phone. 👌

Hope that this was helpful. If you have any suggestions on this post, leave a message in the comments below.

For future guides make sure to follow me on Twitter.

— Austin

--

--