Serverless Phone Number Validation with AWS Lambda, Python and Twilio

February 21, 2020
Written by

Serverless Phone Number Validation with AWS Lambda, Python and Twilio

If your application requests phone numbers from your users, it’s a good idea to ensure that the numbers they provide are valid. The Twilio Lookup API provides an easy way to obtain information about any phone number, such as validity, carrier, line type, and even caller ID information for certain numbers in the United States.

In this article, you will learn how to build a simple serverless function that performs phone validations. The application will be hosted on the AWS Lambda serverless platform and will be written in Python. Even if phone validation isn’t your thing, you will find that the techniques presented in this article can be applied to other APIs from Twilio or even other service providers.

Tutorial Requirements

To follow this tutorial you need the following items:

  • An AWS account. If you don’t have an AWS account yet, sign up for an account for free and enjoy a generous allowance during your first year.
  • A Twilio account. If you are new to Twilio you can create a trial account and start developing for free. Please review the features and limitations of a free Twilio account. If you use this link to register, you will receive a $10 credit when you decide to upgrade your account.
  • A Python interpreter. I will be using Python 3.8, but any recent version will work as well. You can go to python.org to download an installer for your operating system.

For this tutorial, I’m going to assume that you are using a Unix-based operating system. If you are using a Linux or Macintosh computer you will have no problem.

If you are using a Windows computer I recommend that you follow this tutorial on Ubuntu for Windows or Cygwin, both of which give you access to a Unix environment and tools without having to leave the Windows UI.

Creating a Lambda Function

Make sure you have your AWS account created, and then head over to your AWS Console.

First of all, it is a good idea to ensure that the console is set to your preferred region. This is important because Lambda functions are only available within the context of the selected region. Most people choose a region that is geographically close to where they live.

In my case, I went with the “US West (Oregon)” region:

select aws region

With your desired region set, open the “Services” dropdown on the top left and select Lambda:

select lambda service

In the Lambda page click the “Create Function” button to create our phone validation function. Here is the create function page:

create lambda function

On this page select the following options:

  • Author from scratch. This is telling AWS that we will create our function manually, without using any templates or pre-generated code.
  • Under “Function name”, enter a name for the function. I’ve used phone-validate.
  • In the “Runtime” dropdown, select Python 3.8 (or any other recent Python 3 version).

Once you’ve made your selections, click the “Create function” button at the bottom of the page.

Congratulations, you have created your Lambda function! Scroll down to the “Function code” section to see the simple placeholder function that AWS created for us, which should look like the following:

import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

How a Python Lambda Works

We are not going to use this code as is. However, it is useful to familiarize yourself with it, as it shows the structure that Lambda functions written in Python must observe.

The function sits in a module called lambda_function.py and is named lambda_handler. While these names can be changed, it is easier to use them as-is. First, they are the defaults that exist in the Lambda function configuration. And second – being defaults, most people that read your code will expect those names.

The event argument contains the input arguments to the function. In most cases, this is given as a Python dictionary. Later we are going to send a phone number to validate as an argument here.

The context argument contains information about the execution environment sent by AWS into your function. In some situations, the Lambda function can use the information provided in this object, but for this tutorial, we are not going to need to use it.

The return value for this function is a Python dictionary with any data that you’d like to return to the caller. The contents of the return value can be anything your function wants to return, with the only requirement that the dictionary must be JSON compatible.

For very simple functions it is enough to write your logic directly in the web-based code editor provided by AWS. This is unfortunately not sufficient when your function depends on third-party packages that need to be installed.

In our case our function is going to use the Twilio Python Helper library, so we are going to use a different method to create, package and upload our function that will be based on a local development environment configured on your own system.

Setting Up a Python Development Environment

For now, we’ll leave the AWS Lambda console and switch to our own computer. We will put our development environment for this function in a new directory that in my case I’ve decided to call lambda-phone-validate.

The following commands create the directory:

$ mkdir lambda-phone-validate
$ cd lambda-phone-validate

If you’ve worked on Python projects before, you probably know that it is considered a best practice to create a virtual environment where you can install the dependencies of your project. Unfortunately, Lambda functions do not support virtual environments. Lambda functions require your dependencies to be installed directly on the top-level directory of your function, right next to the lambda_function.py file that contains the function’s entry point.

Our function is going to use the twilio package to make calls into the Twilio Lookup API. Instead of creating a virtual environment we are going to install this package in a way you are probably not used to see:

$ mkdir package
$ pip install -t package twilio

The -t option given to pip is short for --target. It tells pip to install the requested packages in the directory given next. We are passing package, a sub-directory that I just created, so the twilio package and its dependencies will be installed in that directory. Note that in some distributions of Python (particularly those originating from Debian and/or Ubuntu) the installation command above may fail with an error. In that case, try the command pip install --system -t package twilio instead. After running the above command take a look at the contents of this directory:

$ ls -l package
total 72
drwxr-xr-x  10 mgrinberg  staff    320 Feb 19 19:16 PyJWT-1.7.1.dist-info
drwxr-xr-x   3 mgrinberg  staff     96 Feb 19 19:16 __pycache__
drwxr-xr-x   4 mgrinberg  staff    128 Feb 19 19:16 bin
drwxr-xr-x   7 mgrinberg  staff    224 Feb 19 19:16 certifi
drwxr-xr-x   9 mgrinberg  staff    288 Feb 19 19:16 certifi-2019.11.28.dist-info
drwxr-xr-x  43 mgrinberg  staff   1376 Feb 19 19:16 chardet
drwxr-xr-x  10 mgrinberg  staff    320 Feb 19 19:16 chardet-3.0.4.dist-info
drwxr-xr-x  11 mgrinberg  staff    352 Feb 19 19:16 idna
drwxr-xr-x   8 mgrinberg  staff    256 Feb 19 19:16 idna-2.9.dist-info
drwxr-xr-x  13 mgrinberg  staff    416 Feb 19 19:16 jwt
drwxr-xr-x  10 mgrinberg  staff    320 Feb 19 19:16 pytz
drwxr-xr-x  11 mgrinberg  staff    352 Feb 19 19:16 pytz-2019.3.dist-info
drwxr-xr-x  21 mgrinberg  staff    672 Feb 19 19:16 requests
drwxr-xr-x   8 mgrinberg  staff    256 Feb 19 19:16 requests-2.23.0.dist-info
drwxr-xr-x   8 mgrinberg  staff    256 Feb 19 19:16 six-1.14.0.dist-info
-rw-r--r--   1 mgrinberg  staff  34074 Feb 19 19:16 six.py
drwxr-xr-x  11 mgrinberg  staff    352 Feb 19 19:16 twilio
drwxr-xr-x   8 mgrinberg  staff    256 Feb 19 19:16 twilio-6.35.4-py3.8.egg-info
drwxr-xr-x  16 mgrinberg  staff    512 Feb 19 19:16 urllib3
drwxr-xr-x   8 mgrinberg  staff    256 Feb 19 19:16 urllib3-1.25.8.dist-info

Interesting, don’t you think?

The unfortunate side effect of using this install technique is that the package directory is cluttered with all these Python packages. Other than that, if you include this directory in the Python import path this works in the same way as having a virtual environment.

Validating Phone Numbers with Twilio Lookup

We now have reached the fun part of this tutorial, which is to write the actual function.

With your favorite text editor or IDE, create a new file named lambda_function.py in the lambda-phone-validate directory, while making sure you leave all those Python dependencies that we installed earlier alone.

Here is the Lambda function that we are going to use to validate phone numbers:

from twilio.rest import Client


def lambda_handler(event, context):
    twilio = Client()
    phone_number = event.get('phone_number')
    if not phone_number:
        return {'error': 'phone_number argument not provided.'}
    try:
        response = twilio.lookups.phone_numbers(phone_number).fetch(
            type=['carrier', 'caller-name'])
        return {'carrier': response.carrier,
                'caller_name': response.caller_name}
    except:
        return {'error': 'invalid phone number'}


if __name__ == '__main__':
    import pprint
    import sys

    response = lambda_handler({'phone_number': sys.argv[1]}, None)
    pprint.pprint(response)

Our function starts by creating a Twilio client instance using the twilio.rest.Client() class. This object knows how to make requests to the Twilio API. Be aware that Twilio requires all clients to authenticate. Credentials can be given as arguments to the Client() class, or they can also be defined in environment variables. You will learn how to authenticate with environment variables in the next section.

As mentioned above, the event dictionary passed to the Lambda function contains the inputs. We look for a phone_number key containing the phone number that we need to validate. If this key does not exist, then we return a dictionary containing just an error key.

Once we have the Twilio client and the phone number we can issue the validation request. The Twilio documentation is a great resource when you need to learn how to make a request to a specific API.

The example Lookup with International Formatted Number gives us the code that we need to use, which requires passing the phone number to the twilio.lookups.phone_numbers() and then fetching a response. The Lookup API provides two types of information for valid numbers, carrier and caller ID data. For this example, we are requesting both.

If the number is invalid, then the call is going to raise an exception. We handle this case with a try / except block, and once again we return a dictionary with an error key. If the call succeeds, that means that the number is valid. In that case, the returned response contains the carrier and caller_name properties that we requested.

The if statement below our function is not going to be used in our Lambda function, but it has the important function of allowing us to test our function locally, without having to upload it to AWS first. You will see how to do that in the next section.

Testing the Lambda Function

We are now ready to test the function!

As mentioned earlier, the Twilio client will not work without authenticating with the Twilio servers first. The Twilio authentication consists of two values, the “Account SID” and the “Auth Token”.

After you create your account and log in you will have access to the Twilio Console, where you can see your authentication credentials:

twilio account credentials

The Twilio Python Helper library looks for these credentials in the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN environment variables, so now you need to set these two variables as follows:

export TWILIO_ACCOUNT_SID="<your-account-sid>"
export TWILIO_AUTH_TOKEN="<your-auth-token>"

You can define these two variables directly on your terminal if you like, but if you plan on using them for other projects it makes more sense to add them to your .bashrc or similar file so they are set automatically on every terminal that you open.

You need to make sure your Account SID and Auth Token are kept secret because anybody that has access to them can make requests to the Twilio API on your behalf. You should never enter these credentials on a computer that you share with other people.

The conditional statement at the bottom of the lambda_function.py file allows us to run the lambda function locally, and pass the first argument as the phone number, in E.164 format, which includes the country code. For example, to validate the US number 234-567-8900 you can run the following command:

$ PYTHONPATH=package python lambda_function.py +12345678900
{'caller_name': {'caller_name': 'XTELCO',
                 'caller_type': 'BUSINESS',
                 'error_code': None},
 'carrier': {'error_code': None,
             'mobile_country_code': None,
             'mobile_network_code': None,
             'name': '...',
             'type': 'landline'}}

Now let’s take the last 0 from the number and try to validate again:

$ PYTHONPATH=package python lambda_function.py +1234567890
{'error': 'invalid phone number'}

Are you wondering how are we able to import from the twilio package without having formally installed it in a virtual environment? The package is now installed in the package sub-directory. Notice that we are setting the PYTHONPATH environment variable to package, so that this directory is searched when importing packages.

Deploying the Lambda Function

If you were able to make calls into the Twilio Lookup API locally as shown in the previous section, you are now ready to deploy the function to AWS and make it serverless!

To deploy the function to the cloud we need to create a deployment package. This is a zip file that has our lambda_function.py file plus all the dependencies that we installed on the directory. Let’s start by making a zip file with all the dependencies:

$ cd package
$ zip -r ../lambda_function.zip .
$ cd ..

Then we add the lambda_function.py file:

$ zip lambda_function.zip lambda_function.py

The lambda_function.zip file now has the source code for our function plus all the dependencies. Let’s go back to the function in the AWS Console. In the “Function code” section follow these steps:

  • Change the “Code entry type” dropdown to “Upload a .zip file
  • Click the “Upload” button and select the lambda_function.zip file you created above
  • Click the “Save” button

upload lambda package

Before we can run the function, we have to configure the two environment variables with the Twilio credentials. Scroll down to the “Environment variables” section and click the “Edit” button to define the two environment variables:

edit lambda environment variables

After you add the two variables press the “Save” button to store them (AWS stores your environment variables encrypted to keep them secure).

The function is now ready to be used, so let’s give it a quick test. Scroll all the way to the top and locate the “Test” button. To the left of that button, there is a dropdown that reads “Select a test event”. In that dropdown, select “Configure test events”:

configure lambda test event

The configure test event page is where you define the inputs that you want to send to the function. AWS generates an example event with three keys. We are going to replace this with our own event dictionary:

{
    "phone_number": "+12345678900"
}

Give this event a name such as “validate” and then click the “Create” button at the bottom.

With the event selected in the dropdown, click the “Test” button to execute the function and see the response:

lambda test execution results

Congratulations, your serverless function is now deployed and working.

Using The Lambda Function

While running the function with the “Test” button in the AWS Console is great for testing, you will probably want to invoke it from an application when you actually need to carry out a phone number validation.

There are two main ways for an application to trigger the execution of this function: as a direct Lambda execution or through an API Gateway.

A direct Lambda execution is the simplest method. To use this method you will need to obtain access keys for your AWS account to be configured on your system, as authentication is required with this method. You can then use any of the available SDKs for AWS. For example, this is how you would invoke the function using Boto3, the AWS SDK for Python:

import boto3

client = boto3.client('lambda')
response = client.invoke(
    FunctionName='phone-validate',
    InvocationType='Event',
    Payload=json.dumps({'phone_number': '+12345678900'})
)

An invocation with API Gateway is more involved (and also more expensive!). You will need to set up a trigger for your Lambda function and associate it with an endpoint defined in the API Gateway service. This method has a much bigger setup effort, but it allows you to expose your Lambda function as an HTTP endpoint that can be invoked by any standard HTTP client. If you are interested in exploring this option, AWS has several tutorials that explain this option in detail.

Conclusion

In this tutorial, you have learned how to create an AWS Lambda serverless function that invokes a Twilio service, in this case, the Lookup API (though the process is the same if you want to use other APIs). You have seen how to test your Lambda function locally, and how to package it along with all its dependencies and deploy it.

I hope this tutorial served as a good introduction to working with serverless functions on the AWS platform. I can’t wait to see what you build with these technologies!

Miguel Grinberg is a Python Developer for Technical Content at Twilio. Reach out to him at mgrinberg [at] twilio [dot] com if you have a cool Python project you’d like to share on this blog!