Receiving Twilio Webhooks Using DigitalOcean Functions

May 17, 2023
Written by
Dotun Jolaoso
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Mia Adjei
Twilion

Receiving Twilio Webhooks Using DigitalOcean Functions

In this tutorial, we’ll explore how to receive Twilio webhooks using DigitalOcean Functions, enabling you to build powerful and flexible applications that interact with Twilio. Twilio webhooks provide a seamless way to receive real-time notifications from Twilio. By leveraging DigitalOcean Functions, a serverless computing platform, you can handle Twilio webhooks and perform custom actions based on the received data. For example, you can trigger a CI/CD workflow, such as Github Actions, whenever your Twilio number receives an SMS message.

Technical requirements

To follow along, you’ll need the following:

  • A free Twilio account.
  • A Python Development Environment running Python 3.9+. You need to have the virtualenv package running locally as well.
  • A DigitalOcean account. You should also have doctl, which is DigitalOcean’s command line interface tool, installed and running locally. You can read more about setting it up here.

Creating a DO Function

To get started with creating a DO Function, you will be making use of the doctl command line tool. However, you first need to install support for serverless functions. You can do that by running the following command:

$ doctl serverless install

This will install and download the serverless extension. Once that is completed, you are now ready to initialize a new function project using a helper command that the doctl tool provides.

To work with DigitalOcean Functions via the command line using doctl, you need to connect doctl to a Functions namespace. Namespaces are used to isolate and organize functions and their settings. If this is your first time working with DO Functions, you’ll need to create a namespace before you can connect it and start deploying Functions. You can do this by running the following command:

doctl serverless namespaces create --label example-namespace --region nyc1

The --label flag is used to specify the name of the namespace while the --region flag indicates which region the namespace should be in. You can learn more about creating namespaces here.

After doing that, you can now connect to the namespace by running the command below:

$ doctl serverless connect

You can now create functions and deploy them to your namespace.

Next, from the directory where you want your project to reside, run the following to initialize a sample function project.

$ doctl serverless init --language python twilio

This will create a twilio project directory that contains a project.yml configuration file, a packages directory containing the sample package, a hello function directory, and the sample “Hello world” function code.

Here’s an outline of what the current directory structure looks like:

twilio/
├── packages
│   └── sample
│       └── hello
│           └── hello.py
└── project.yml

This is good for a start, however, you’ll need to rename some of the directories and files to align with the purpose of the function.

  1. Rename the sample package to twilio
  2. Rename the hello function directory to webhooks
  3. Rename the hello.py file to __main__.py . This is in accordance with what DigitalOcean recommends where the file containing the handler function to be executed is named __main__.py.You can read more about it here

Here’s an outline of what the new directory structure is supposed to look like:

twilio/
├── packages
│   └── twilio
│       └── webhooks
│           └── __main__.py
└── project.yml

Next, replace the project.yml file at the root of the project’s directory with the following:

targetNamespace: ''
parameters: {}
packages:
  - name: twilio
    environment:
      TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN},
      TWILIO_WEBHOOK_URL: ${TWILIO_WEBHOOK_URL}
    parameters: {}
    annotations: {}
    actions:
      - name: webhooks
        runtime: 'python:default'
        web: raw

The packages and actions names are now consistent with the naming used for the directories. You’ve also added the environment variables the function needs to run. The environment variables will be fetched from a .env file you’ll be creating shortly using Templating.

Add the following code to the __main__.py file:

import os
from http import HTTPStatus
from urllib.parse import parse_qs
from twilio.request_validator import RequestValidator


def main(args, context):
    body = args.get("http").get("body", {})
    data = parse_qs(body, True)
    data = {k: v[0] for k, v in data.items()}  # convert values to strings
    validator = RequestValidator(os.getenv('TWILIO_AUTH_TOKEN'))
    url = os.getenv('TWILIO_WEBHOOK_URL')
    header = args.get("http").get("headers").get("x-twilio-signature", "")
    if not validator.validate(url, data, header):
        return {
            "statusCode": HTTPStatus.BAD_REQUEST,
            "body": "invalid signature"
        }

    return {
        "statusCode": HTTPStatus.ACCEPTED,
        "body": body
    }

The first argument args to the main() function is typically an HTTP request event or web event that contains information about the request. The second parameter context contains information about the function’s execution environment.

Within the main() function, the request body is fetched from the args dictionary using args.get(“http”).get(“body”). If the body key is not found, it assigns an empty dictionary to the body variable. If it is found, the returned value is a string. The parse_qs function is then used to parse the body into a dictionary.

To verify the request is authentic, you can use the RequestValidator class included in the Twilio Helper library for Python. This class is initialized by passing in your Twilio Auth Token.

The validate() method on the class is called passing in the webhook URL, which is obtained as an environment variable using os.getenv, a dictionary with the request body, and the signature contained in the X-Twilio-Signature header. If this method returns False, the request is aborted with a status code of 400.  

Deploying a DO Function

Functions with external dependencies require a build script and a requirements.txt file before they can be deployed. You can read more about why they are required here. Within the packages/twilio/webhooks directory, create a requirements.txt and build.sh file.

Note: Depending on the OS you’re currently running, you might need to give the right permissions to the build.sh file. On Mac/Linux, you can do that by running the command:

chmod +x  packages/twilio/webhooks/build.sh

Add the twilio library as a dependency to the requirements.txt file:

twilio==8.2.0

The build.sh script is used to create a virtualenv and install the packages. Paste the following in the file:

#!/bin/bash

set -e

virtualenv --without-pip virtualenv
pip install -r requirements.txt --target virtualenv/lib/python3.9/site-packages

Environment Variables

Before you can deploy the function to DigitalOcean, you need to configure your environment variables. Head over to your Twilio Console, and take note of your Auth Token.

Twilio Credentials

Head back to the root of the project’s directory and create a .env file. Edit the file with the following credentials:

TWILIO_AUTH_TOKEN=xxxx
TWILIO_WEBHOOK_URL=xxxx

Replace the TWILIO_AUTH_TOKEN with the actual value you noted from your Twilio Console.

For the TWILIO_WEBHOOK_URL field, you can use a random string as the value there temporarily. You’ll be updating that field shortly after the function has been deployed.

Next, cd out of the project directory and run the command below to deploy the function:

doctl serverless deploy twilio

Once the function has been successfully deployed, you can now fetch the URL where the function was deployed by running the command below:

doctl sbx fn get twilio/webhooks --url  

The command outputs the URL details:

https://faas-fra1-afec6ce7.doserverless.co/api/v1/web/fn-2932edc9-6e03-47ae-bd94-10f89c11a51f/twilio/webhooks

You can now head back to the project’s directory, and update the TWILIO_WEBHOOK_URL field in the .env file with the actual URL. Next, cd out of the project and then redeploy the project by running the deploy command:

doctl serverless deploy twilio

Once you have deployed the function, you can get the function URL by running the following command:

doctl sls fn get twilio/webhooks --url

You can now take the function URL and configure your Twilio account with it. You can learn more about doing that here. 

Conclusion

In this tutorial, you’ve seen how to get started with DigitalOcean Functions and deploy a serverless function you can use for processing incoming webhooks from Twilio. You’ve also seen how convenient it is to deploy the function by running a command. This tutorial can serve as the foundation for building more complex serverless functions with DigitalOcean and Twilio.

Dotun is a backend software engineer who enjoys building awesome tools and products. He also enjoys technical writing in his spare time. Some of his favorite programming languages and frameworks include Go, PHP, Laravel, NestJS, and Node.

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