Recording and Saving Outbound Voice Calls with Python, Twilio and Dropbox

September 09, 2020
Written by
Dotun Jolaoso
Contributor
Opinions expressed by Twilio contributors are their own

Recording and Saving Outbound Voice Calls with Python, Twilio and Dropbox

In this tutorial, we’ll be looking at how to record outbound voice calls via the Twilio Programmable Voice API as well as uploading the recordings to Dropbox using the Dropbox API. So basically, person #1 would call your Twilio number, ask to call a person #2 by entering their phone number, and then the call between person #1 and person #2 would be recorded and sent to Dropbox.

Technical requirements

To follow along, you’ll need the following:

  • A free Twilio Account. If you use this link to register, you will receive $10 credit when you upgrade to a paid account.
  • Python Development Environment
  • Ngrok. This will make the development version of our application accessible over the Internet.
  • A free Dropbox Account.

Creating a Python environment

Let’s create a directory where our project will reside. From the terminal, run the following command:

$ mkdir twilio_outbound_dropbox

Next, cd into the project directory and run the following command to create a virtual environment.

$ python -m venv venv

To activate the virtual environment, run the following command:

$ source venv/bin/activate

If you are using a Windows computer, then the activation command is different:

$ venv\Scripts\activate

Next, we’ll install all the dependencies our project will be needing:

  • Flask: A Python web framework.
  • twilio: A Twilio helper library for interacting with Twilio’s REST API.
  • requests: A Python library for making HTTP requests.
  • dropbox: A Python library for integrating with Dropbox’s API v2.
  • python-dotenv: A library for importing environment variables from a .env file.

Next, run the following command to install all of the dependencies at once:

$ pip install flask twilio requests dropbox python-dotenv

Setting up Twilio

Before we can get started with making outbound voice calls via Twilio, you’ll need to have a voice enabled Twilio phone number. If you don’t already have one, we’ll go over how you can purchase one in this section.

Head over to the Phone numbers section on the Twilio Dashboard. Click the “+” sign to start the process of buying a new number.

Purchase a Twilio phone number

You’ll be presented with a screen similar to the one below

Buy a number

Select the “Country” you want the phone number to be from. Fill the “Number” and “MATCH TO” fields to suit your needs. For the “Capabilities” section, make sure the “Voice” checkbox is selected. If you would like the phone number to support other communication capabilities besides voice calls, you can select any other options you wish.

Next, you’ll be presented with a list of phone numbers along with their location, type, capabilities as well as pricing that match your search request. Click “Buy” to purchase the number that you like. Note that if you have a trial account you will be using your trial account funds to make this purchase.

Buy the selected Twilio number

Setting up Ngrok

Twilio makes use of webhooks to asynchronously notify our application when certain events occur. To be able to successfully respond to and process incoming calls, we need to configure an endpoint where Twilio can send an HTTP POST request whenever our Twilio phone number receives an incoming call.

Since we’ll be building our application locally, there is no way for Twilio’s requests to reach our application. Thankfully, this is where Ngrok comes in handy, allowing us to set up a temporary public URL so that our app is accessible over the web.

Run the following command on your terminal window to start ngrok:

$ ngrok http 5000

In this command, 5000 refers to the port your Flask application will eventually be listening on.

You should now be presented with a screen similar to the one below:

ngrok screenshot

Take note of the https:// “Forwarding” URL as we’ll be making use of it shortly.

Next, head back to the Active Numbers section on your Twilio console and select the number you purchased earlier.

phone numbers dashboard

You’ll be presented with a screen that shows you details about the phone number. Under the “Voice & Fax” section, append the Ngrok URL we noted earlier with “/inbound/voice/call” and then paste it in the “A CALL COMES IN” field. Ensure the request method is set to HTTP POST and then click the “Save” button at the bottom of the page to save the settings. This is the endpoint Twilio will send a request to whenever a call is placed to our Twilio number.

set voice webhook URL

Creating & Recording Outbound Voice Calls

At the root of your project’s directory, create a main.py file and edit the file with the following code:

import os
import dropbox
import requests
from flask import Flask, request, Response
from dotenv import load_dotenv
from twilio.twiml.voice_response import Gather, VoiceResponse, Dial

load_dotenv()
app = Flask(__name__)


@app.route('/inbound/voice/call', methods=['POST'])
def incoming_voice_call():
    response = VoiceResponse()
    gather = Gather(action='/outbound/voice/call', method='POST')
    gather.say('Please enter the number to dial, followed by the pound sign')
    response.append(gather)
    response.say('We didn\'t receive any input. Goodbye')
    return str(response)


if __name__ == '__main__':
    app.run()

At the top of this file, we’ve imported all the dependencies our project will be needing.

The load_dotenv() function loads our environment variables from a .env file. We’ll be creating one shortly.

The incoming_voice_call() function will be executed when our Twilio phone number receives a phone call. Let’s go over what’s happening in this function. We’ve defined a predefined message format and set of instructions to speak using the Twilio Markup Language (TwiML), which at its core is an XML document with special tags defined by Twilio.

The Python Twilio library provides some helper classes for working with TwiML. One of such classes is the Gather class which is used for working with TwiML’s <Gather> verb. This verb is used to collect digits from the caller. We prompt the user to enter the phone number they’d like to dial on their keypad, followed by a “#” symbol.

After they’ve entered the “#” symbol, Twilio will submit the sequence of numbers in a POST request back to the application at the relative URL given in the action parameter. If the user does not enter anything for 5 seconds, then the gather command times out and execution continues to the next instruction, which in this case is the <Say> verb, by which Twilio informs the user that an input wasn’t provided before the call ends.

Next, let’s add the function that will be responsible for making the outbound call, once Twilio makes the second request with the phone number that the user entered.

Just below the incoming_voice_call() function, add the following functions:

@app.route('/outbound/voice/call', methods=['POST'])
def make_outbound_call():
    phone_number = request.form['Digits']
    response = VoiceResponse()
    dial = Dial(record=True, recording_status_callback='/recording/callback', recording_status_callback_event='completed')
    dial.number(f"+{phone_number}", url='/seek/consent')
    response.append(dial)
    return str(response)


@app.route('/seek/consent', methods=['POST'])
def seek_consent():
    response = VoiceResponse()
    response.say('This call is going to be recorded. You can hang up if you\'re not okay with it')
    return str(response)

How does our application obtain the phone number that was entered by the user?  Twilio includes it in the payload of the POST request with a key of Digits. This time we’re making use of TwiML’s <Dial> verb, which is used to connect the current caller to another party during an active call.  Let’s go over what each argument to the Dial class does:

  • The record argument is set to True to indicate to Twilio that the call between these two parties should be recorded.
  •  recording_status_callback is the relative URL we want Twilio to make a request to once the recording has been completed.
  • recording_status_callback_event is used to specify which recording status changes should trigger a callback to our application. Available options are in-progress, completed and absent. Note that if this parameter isn’t provided, the default value is completed.

Next, using the number function on the dial object which is an alias for TwiML’s <Number> noun, a call is then placed to the phone number. We also specify a url argument to this function which allows us to provide a relative URL that Twilio will invoke on the called party’s end after they answer, but before the two parties are connected.

The endpoint given in the url parameter is implemented in the seek_consent function, and must also return TwiML with instructions to follow by Twilio. Here we inform the called party that the call is going to be recorded, in accordance with privacy laws and regulations in many countries.

Setting up Dropbox

To be able to use the Dropbox API, we need to create an app on Dropbox. Head over to the Dropbox Developers page and select “Create app”. You’ll be presented with a screen similar to the one below:

Dropbox app creation

For the “Choose an API” section, select the “Scoped access” option. Select “Full Dropbox” as the type of access the application will be needing. Provide any name you find befitting under the “Name your app” section. Once you’re done providing all the details, Click “Create app” at the bottom of the page.

After the app has been created, head over to the “Permissions” tab and select the “files.content.write” permission for the app. This will enable the app to be able to create, modify and delete file data within Dropbox. Click “Submit” at the bottom of the page so that the changes can take effect.

Dropbox permissions

Next, head back to the “Settings” tab and within the “OAuth 2” section, set the “Access token expiration” to “No expiration” and then click the  “Generate” button just below the “Generated access token” header.

Dropbox generate access token

This will generate an access token tied to your Dropbox account. Take note of the generated token as we’ll be adding it to our environment variables shortly.

Uploading to Dropbox

Create a .env file at the root of your project and add a DROPBOX_ACCESS_TOKEN key. Set the value to the access token Dropbox generated for you in the previous section.

DROPBOX_ACCESS_TOKEN=xxxxx

Next, within the main.py file we created earlier, add the following code just below the seek_consent function:

@app.route('/recording/callback', methods=['POST'])
def upload_recording():
    recording_url = request.form['RecordingUrl']
    recording_sid = request.form['RecordingSid']
    dropbox_client = dropbox.Dropbox(os.getenv('DROPBOX_ACCESS_TOKEN'))
    upload_path = f"/twilio-recording/{recording_sid}.mp3"
    with requests.get(recording_url, stream=True) as r:
        dropbox_client.files_upload(r.raw.read(), upload_path)
    return Response(), 200

Once the call recording has been completed, Twilio will make a request to the /recording/callback endpoint that we passed in the recording_status_callback parameter of the Dial object. Within the upload_recording function, the dropbox_client is used to interact with the Dropbox API.

The DROPBOX_ACCESS_TOKEN we added to our environment variables is used to authenticate against Dropbox. Next, the RecordingUrl and RecordingSid are obtained from the payload that was sent to the endpoint by Twilio. The upload_path variable is then set to the file path the recording will have once uploaded to our Dropbox account.

Using the requests library, a GET request is made to the recording_url, indicating that the response should not be downloaded immediately, so that the large audio file does not use up a lot of memory. This is done by specifying the stream argument to be True. The files_upload method on the dropbox_client is used for uploading the file to Dropbox. The method accepts the raw attribute of the response along with the upload_path as arguments.

Bringing it all together, the final main.py will look like this:

import os
import dropbox
import requests
from flask import Flask, request, Response
from dotenv import load_dotenv
from twilio.twiml.voice_response import Gather, VoiceResponse, Dial

load_dotenv()
app = Flask(__name__)


@app.route('/inbound/voice/call', methods=['POST'])
def incoming_voice_call():
    response = VoiceResponse()
    gather = Gather(action='/outbound/voice/call', method='POST')
    gather.say('Please enter the number to dial, followed by the pound sign')
    response.append(gather)
    response.say('We didn\'t receive any input. Goodbye')
    return str(response)


@app.route('/outbound/voice/call', methods=['POST'])
def make_outbound_call():
    phone_number = request.form['Digits']
    response = VoiceResponse()
    dial = Dial(record=True, recording_status_callback='/recording/callback', recording_status_callback_event='completed')
    dial.number(f"+{phone_number}", url='/seek/consent')
    response.append(dial)
    return str(response)


@app.route('/seek/consent', methods=['POST'])
def seek_consent():
    response = VoiceResponse()
    response.say('This call is going to be recorded. You can hang up if you\'re not okay with it')
    return str(response)


@app.route('/recording/callback', methods=['POST'])
def upload_recording():
    recording_url = request.form['RecordingUrl']
    recording_sid = request.form['RecordingSid']
    dropbox_client = dropbox.Dropbox(os.getenv('DROPBOX_ACCESS_TOKEN'))
    upload_path = f"/twilio-recording/{recording_sid}.mp3"
    with requests.get(recording_url, stream=True) as r:
        dropbox_client.files_upload(r.raw.read(), upload_path)
    return Response(), 200


if __name__ == '__main__':
    app.run()

Testing

To start the application, open a second terminal window, activate the virtual environment and run the following command:

(venv) $ python main.py

Make sure you have ngrok running as shown earlier. Note that every time you restart ngrok a new temporary tunnel URL will be created, so you will need to go back to the Twilio console and update the webhook URL for your Twilio phone number.

Next, place a call to your Twilio phone number and enter the phone number you would like to call including the country code, followed by the “#” symbol. If the other party accepts the recording consent warning by staying on the line the call between you and them will be established. Once the call ends, a copy of the recording will be uploaded to your Dropbox account.

Dropbox call recordings

Conclusion

In this tutorial, we’ve learnt how to record outbound voice calls via Twilio, along with uploading these recordings to Dropbox using the Dropbox API. It is important to note that there may be certain legal implications that apply where you live when recording voice calls, so be sure you know what those are. Twilio has some information about this here.

The source code for this tutorial can be found here on Github.

Dotun Jolaoso

Website: https://dotunj.dev/
Github: https://github.com/Dotunj
Twitter: https://twitter.com/Dotunj_