Building Voicemail with Twilio and Ruby

January 28, 2021
Written by
Reviewed by

building-voicemail-twilio-ruby-on-rails

One useful and common way to use Twilio is to create a phone number which forwards to your real phone. You can hand out the Twilio number to colleagues or customers and take business calls on your personal phone without worrying about handing out your real phone number. This also gives you a lot of options for how to handle these calls - you have the full power of Twilio at your disposal.

In this post I'll show you how to create a voicemail system that will jump in when your cell number is busy, or when you don't answer the call. It will play a message to your caller, then record their message and text you a link to the recording when it's done.

Primer: How Twilio Handles Calls

When someone dials your Twilio number, an HTTP request is sent to a URL that you provide. The web server handling that URL should respond with TwiML which will instruct Twilio what to do next. To program the behaviour of a Twilio number you will need to create a web server which can take requests from Twilio and respond with TwiML, a language based on XML that helps you define how Twilio should handle the call.

schema of how Twilio interacts with your application to handle phone calls

In this post I'll use Ruby with Ruby On Rails as the web framework, but you could use the same approach with any programming language, for example here is how to do it with Java.

Requirements

To create the same application you need to have

Creating your new codebase

In an empty directory, initialize your project using the Rails CLI:

rails new VoiceMail
cd VoiceMail

This creates a fresh Ruby On Rails project. Open the project with your favorite text editor and let's get cracking.

Dependencies and configuration

For this application we will use:

Open the Gemfile at the root of your project with your text editor and add the code below.

# Gemfile
gem 'figaro'
gem 'twilio-ruby'

In your console install the gems and run the Figaro initializer

bundle install
bundle exec figaro install

The figaro install command will create a new configuration file config/application.yml and append your .gitignore to tell git not to track this new file.

This is where you are going to put all your environment variables.

In this file add the following lines and replace each X with the corresponding values from your Twilio console. You must also add your own phone number - not your Twilio phone number - in the E.164 format.

# config/application.yml
twilio_account_sid: "X"
twilio_auth_token: "X"
my_phone_number: "+X"

Make sure you have run $ bundle exec figaro install otherwise your secrets might be compromised when pushing your code to git if you include the config file.

We can now create an initializer for our Twilio Client.

Under the folder config/initializers create a new file called twilio.rb.

# config/initializers/twilio.rb
Twilio.configure do |config|
  config.account_sid = ENV["twilio_account_sid"]
  config.auth_token = ENV["twilio_auth_token"]
end

This will create the Twilio client without having to hard code credentials or specify where they are.

Building up the app

One thing I love about programming is the way it forces you to break a problem apart into tiny pieces. So what are the pieces of this app? You need to instruct Twilio to:

  • Answer incoming calls and redirect to your real cell phone number,
  • If you answer it, nothing more needs to be done. However if the line is busy, or the call rings out, play a message and start recording,
  • Once the recording is finished, send a notification that there is a new recording to listen to.

Each of these will be handled by a different endpoint on the web server, so let's create them now in our routes.

Open `config/routes.rb` and fill it with

# `config/routes.rb`
Rails.application.routes.draw do
  get '/initial-answer', action: :initial_answer, controller: 'twilio'
  get '/handle-unanswered-call', action: :handle_unanswered_call, controller: 'twilio'
  post '/recordings', action: :recordings, controller: 'twilio'
end

As you can see there are three routes for our three actions and they all belong to a controller named twilio.

Let’s create it under our controllers folder as app/controllers/twilio_controller.rb

Here is a minimalist version of this controller with empty functions that we will fill right after.

# app/controllers/twilio_controller.rb
class TwilioController < ApplicationController
  skip_before_action :verify_authenticity_token

  def initial_answer
  end

  def handle_unanswered_call
  end

  def recordings
  end
end

Note here the skip_before_action :verify_authenticity_token. This line of code without other checks can lead to security issues in production. Here is a way to secure your application when going into prod.

Answering and redirecting incoming calls

In our TwilioController we will fill the initial_answer method first. This method returns the TwiML (Twilio Markup Language) for answering the call and redirecting to your cell phone:

# app/controllers/twilio_controller.rb
def initial_answer
  response = Twilio::TwiML::VoiceResponse.new
  initial_answer = response.dial(number: ENV['my_phone_number'],
                                 timeout: 10,
                                 action: "/handle-unanswered-call",
                                 method: "GET")
  render xml: initial_answer
end

There's a few things going on here, so let's step through it:

  • We create a VoiceResponse that will send back TwilML to Twilio.
  • We use the Dial TwiML verb to connect out to another number.
  • The Dial verb takes a number of attributes:
    • the number to transfer the incoming call to, here our own we specified in the environment variable ENV['my_phone_number'].
    • the timeout for deciding when the call is unanswered. This timeout is configured as 10 seconds but feel free to change it (noting that Twilio adds a buffer of up to 5 seconds to account for the time taken to establish the call),
    • The action and method tell Twilio where to look for the next set of TwiML instructions after the call is established, rejected or timed out.
  • Finally we render this TwilML we created as xml using the helper render

This is a good point to check that everything is working correctly in the project setup.

Start the server. You can do this with

rails s

 in a terminal which will serve your application on localhost:3000.

Once it's started up, hit http://localhost:3000/initial-answer in your browser. It should respond with:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Dial action="/handle-unanswered-call" 
        method="GET"
        timeout="10">
    +xxxxxxxxxxxx
  </Dial>
</Response>

If you see that, you're good to go! If you see *almost* that but without your cell phone number then that means the my_phone_number environment variable hasn't been found so double-check that you are setting it correctly.

The voicemail message

Now let’s look at our handle_unanswered_call method. In TwilioController we will add the following lines to the method

# app/controllers/twilio_controller.rb
def handle_unanswered_call
  if params["DialCallStatus"] == "busy" || params["DialCallStatus"] == "no-answer" 
    render xml: voicemail_twiml
  end
end

Here we check from our request params from Twilio to see if we are in the case when we are busy or haven’t answered the call.

We do the check and if the condition matches we return the content of the voicemail_twiml function. But wait, where is this method ?

That’s right, we haven’t created it yet.

As this method doesn't need to be able to speak directly with the world wide web, we will define it as private.

Before the closing end of our TwilioController, add the method under private as follows:

# app/controllers/twilio_controller.rb
private

def voicemail_twiml
  response = Twilio::TwiML::VoiceResponse.new
                
  response.pause(length: 2)
          .play(url: '/message.mp3')
          .record(play_beep: true, action: '/recordings')                                
end                                

This generates TwiML that pauses for a couple of seconds before playing a message and starting a recording. You could record your own message.mp3or use this one which is a classic. However you get it, put message.mp3 file in /public. Rails will automatically serve static files from that directory.

You could also use a Say verb if you don't want to record your own message - replace the line .play(url: '/message.mp3') with

.say(message: "Sorry I can't take your call at the moment, please leave a message")

The Record verb takes an action option that tells Twilio which endpoint to hit once the recording is complete. Here we are using /recordings

Handling completed recordings

The last endpoint is /recordings, called after someone has left you a message. Useful parameters passed by Twilio in here are:

  • RecordingUrl - you can download the recording from here in wav format, or append .mp3 to fetch it in that format.
  • From - the caller's number
  • To - the number they dialled, ie your Twilio number.

The Ruby code to handle this will take those parameters and send an SMS to your cell phone number with all the details:

#app/controllers/twilio_controller.rb

def recordings
  mp3_recording_url = params["RecordingUrl"] + ".mp3"
  sms_notification = "You got an answerphone message from #{params["From"]} - listen here: #{mp3_recording_url}"

  client = Twilio::REST::Client.new
  client.messages.create(from: params["To"],
                         to: ENV['my_phone_number'],
                         body: sms_notification)

  head :ok
end

Using your server to handle real phone calls

You need to do three things to get the web server connected and working:

  • Make sure that Twilio can access the server,
  • buy a phone number from Twilio,
  • configure the phone number to call the /initial-answer endpoint when there is an incoming call.

Serving our application to the world

For the first part, you need to make sure that your server can be reached over the internet: localhost isn't going to cut it here. There are a lot of choices for deploying Ruby apps publicly, but a simple way to get things going when testing is to use ngrok - a tool which creates a tunnel so that your localhost server has a public URL.

I use ngrok http 3000 to spin this up, but however you do it, you will need the full URL of the /initial-answer endpoint in the next step.

If you choose to do it using ngrok you should have a server running locally. If you haven’t done so yet, fire it up using

$ rails s

Now that your server is able to communicate with the world, from the Rails perspective we need to tell our application this is okay to handle requests coming from this url.

Since we are in development mode we will do it in the development configuration file but the process is the same for each environment, you just need to do it in the corresponding file.

For dev purposes we will just add before the closing end of config/environments/development

config.hosts << ".ngrok.io"

Note the dot before “ngrok.io”. As your ngrok subdomain will change each time, it tells Rails to allow ngrok.io and all it’s subdomains as hosts.

You can see the full code on my GitHub.

Getting a Twilio Phone Number

If you've already got a Twilio number that you want to use then great. If not then head over to the Twilio console to log in and pick yourself a new phone number. If you don't already have a Twilio account you can sign up for one here. Once you have the number, on the number's configuration page set the public URL of your web server as the webhook for "When a call comes in" - don't forget to set the method to HTTP GET in the console matching the initial_answer method in our TwilioController.

screenshot of the Twilio console with the right settings

Make sure you don't forget to click "Save", and you're finished. Test it out by having someone call your Twilio number - you'll see a call come in from them on your cell phone, which you can answer, reject or ignore as you wish. If they leave a message you will get an SMS with a link to the recording which you should be able to listen to by clicking the link right from your messaging app.

Summing up                                                                               

If you've followed along with this post, you now have a working voicemail for your Twilio number. You have also learned a bit about how Twilio uses webhooks to handle incoming calls, and how to build them out using Ruby and Rails. This is just one possibility for handling incoming calls with Twilio, if voicemail isn't quite what you want, how about...

Whatever you're building with Twilio and Ruby, I'd love to hear about it. Get in touch by email or Twitter.