Publish Twilio Errors to a Slack Channel using Python and Flask

February 12, 2021
Written by
Jamie Corkhill
Contributor
Opinions expressed by Twilio contributors are their own

Publish Twilio Errors to a Slack Channel using Python and Flask

In this article, you’ll learn how to use the Twilio Debugging Events Webhook to build a service that forwards Twilio Error Events to a Slack channel of your choice using Python and Flask. Along the way, you’ll learn how to configure the webhook, create a Slack App, and format Slack messages with rich layouts.

In the image below, you can see an example message sent through Slack in response to an error from Twilio:

Twilio error in Slack channel

Note: The JSON Error Payload is rather large, so it was shortened here for brevity. Additionally, the first characters of the SIDs have been redacted.

Requirements

  • Python 3.6 or newer. You can download a Python interpreter here.
  • A Twilio account with a voice-capable phone number. If you are new to Twilio, you can create a free account. Although a phone number isn’t strictly required to facilitate this build, you’ll use it to trigger a fake error in order to test the service.
  • A Slack account. If you are new to Slack, you can create a free account.

Project Configuration 

Navigate to the directory where you’d like to create your project, and run the following commands:

mkdir twilio-error-slack-publisher
cd twilio-error-slack-publisher

Next, create a virtual environment to isolate dependencies:

python3 -m venv venv

Then, activate the environment, which you can do on a Unix-based system with:

. venv/bin/activate

Or on Windows with:

venv\Scripts\activate

For the latter command, if using PowerShell on Windows, ensure you have an ExecutionPolicy that permits the running of scripts.

Install the required Python dependencies as follows:

pip install flask slack_sdk python-dotenv pyngrok

Slack API Setup

With the project created, you can now move forward with creating the Slack Workspace, Channel, and App. If you already have a Slack workspace that you’d like to use, open it up and create a new channel entitled #twilio-integration. This will be the channel over which you’ll receive alerts.

If you are new to Slack or don’t have a workspace, create one here, which will require that you enter your email, a workspace name (such as “Test Workspace”), a topic your “team” is currently working on, for which you can use “Twilio Integration”, and finally to invite team members, which is a step you can skip. When that process is complete, you should have a workspace set up like the one below:

Slack workspace

To create your Slack App, visit the Slack API Dashboard and select the green Create a custom app button at the center of the page. You’ll be asked to select a name and a workspace, and you can pick the workspace you just created and a name such as “Twilio App”, as depicted below:

Create Slack app

Select Create App at the bottom right-hand corner, and then select Incoming Webhooks under the Basic Information settings page.

Incoming webhooks

Here, set the top-right toggle switch to On and select Add new Webhook to Workspace:

Add new webhook to workspace

Thereafter, select the #twilio-integration channel (which you just created) in the dropdown and then click Allow. You will be redirected back to the Incoming Webhooks page and will be provided with a new Webhook URL in the table, which you’ll need to copy. This URL contains sensitive information and should thus be kept private. As such, you’ll store it in an environment variable momentarily. Slack actively searches for leaked Hook URLs and revokes them.

At the root of your project, create a file entitled .env (notice the preceding dot), and paste the following text within it:

SLACK_WEBHOOK_URL=your-webhook-url

Replace your-webhook-url with the Webhook URL you copied in the earlier step.

Building the Application

Create an app.py file in the project directory with the following boilerplate for this project:

import os
import json
from http import HTTPStatus

from flask import Flask, request
from dotenv import load_dotenv
from slack_sdk.webhook import WebhookClient

load_dotenv()

app = Flask(__name__)

After the relevant import statements, you call load_dotenv() so that the environment variable you saved earlier in the .env file is imported.

When Twilio sends a POST Request to your endpoint after a debugging event occurs, it will pass a variety of parameters, described here, only some of which you’ll be interested in. The ones you care about for this tutorial are described below:

  • Sid - Unique identifier of this Debugger event.
  • AccountSid - Unique identifier of the account that generated the Debugger event.
  • Timestamp -         Time of occurrence of the Debugger event.
  • Level - Severity of the Debugger event. Possible values are Error and Warning.
  • Payload - JSON data specific to the Debugging Event.

These five parameters are the ones you’ll forward to Slack. You’ll have to parse them off the request body and pass them to some function which will take them and fold them into a proper formatting understood by the Slack API. You can begin by defining that function at the bottom of your app.py file.

def send_to_slack(sid: str, account_sid: str, timestamp: str, level: str, payload: str):
    WebhookClient(os.environ.get('SLACK_WEBHOOK_URL')).send(blocks=[
        {
            "type": "section",
            "text": {
                "text": f"A *Twilio Debugger Event* occurred. See details below:",
                "type": "mrkdwn",
            },
        },
        {
            "type": "section",
            "text": {
                "text": (
                    f'*Debugger Event SID:* {sid} \n'
                    f'*Affected Account SID:* {account_sid} \n'
                    f'*Timestamp:* {timestamp} \n'
                    f'*Level:* {level} \n'
                ),
                "type": "mrkdwn"
            }
        },
        {
            "type": "divider"
        },
        {
            "type": "section",
            "text": {
                "text": f"

On the first line, you create a new WebhookClient and pass to it the Slack Webhook URL you saved as an environment variable earlier. You call the send method available on WebhookClient and pass it an array of “blocks”. Blocks are a UI framework offered by Slack to enable the building of rich message layouts, defined as dicts. You can learn more about them here. To compose the layout, you use the following blocks:

  • Section - A flexible block that may be used as a text block or in combination with more complex controls. Learn more about sections here.
  • Divider - A content divider, similar to an <hr> element in HTML. Learn more about dividers here.

Each block expects a type property, and, in some cases, a text property. A Section is denoted with the ”section” type while a divider is denoted with the ”divider” type. In your case, since you’re only showing text, the ”text” property for a Section expects the actual text to show to the screen as well as an indicator of whether it’s plain text or markdown, the latter of which is denoted as ”mrkdwn” under text.type.

This array of blocks creates a top section containing a title, a middle section containing easy-to-read information for the specific event that has occurred, a divider, and a full JSON payload of the error, which will be pretty-printed to the screen.

With this function complete, you can define the endpoint that will call it. Place the following route handler above the send_to_slack function:

@app.route('/errors', methods=['POST'])
def twilio_error_received():
    try:
        send_to_slack(
            request.values.get('Sid'),
            request.values.get('AccountSid'),
            request.values.get('Timestamp'),
            request.values.get('Level'),
            request.values.get('Payload')
        )
    except Exception as e:
        print(str(e))
    finally:
        return '', HTTPStatus.NO_CONTENT

Here, whether an exception occurs or not, you always respond to the POST Request with a 204 No Content status, for you don’t have anything to return from this endpoint to Twilio. In the event that an exception does occur, you simply log it - Twilio doesn’t need to know about it.

Since it’s possible that exceptions may occur due to transient network failures, either on your side or Slack’s, you may want to wrap the call to send_to_slack in an exponential backoff retry strategy for resiliency. That’s out of scope for this article, however.

Running the Application

With the application complete, it’s time to test it. You can run the app on MacOS or Linux with

FLASK_ENV=development flask run

Or on Windows with:

set FLASK_ENV=development
flask run

Then open a second terminal window, activate the Python virtual environment and run:

ngrok http 5000

Ngrok is a tool that creates a secure tunnel to any service running on localhost, and in doing so, it provides you with a public URL allowing you to access that service from across the Internet and test your webhook without doing a complete deployment. With the command above, Ngrok will proxy requests from the public URL to the Flask application running on local port 5000. After running the command, copy the HTTPS forwarding URL as depicted in the image below:

Ngrok screenshot

Navigate to the Twilio Debugger in the Console and save the URL of your endpoint as the Webhook URL. Note that the URL is composed of the Ngrok forwarding URL plus the /errors path of the Flask endpoint.

Now that you have your project configured and running, you’ll need to trigger an error in order to ensure it works correctly. An easy way to do this is to set up an invalid phone number Webhook URL. If you don’t already have one, you’ll need to purchase a voice-capable phone number, so login to the Twilio Console and visit the Buy a Number page. Ensure you have Voice capabilities enabled and click Search:

Buy a Twilio number

When you find a suitable number, select Buy so that it may be provisioned. If you need more information on purchasing phone numbers, visit the relevant page of the Twilio Help Center.

Next, visit the Phone Number Management Console, select your phone number, and scroll to the Voice webhook. You want to intentionally cause Twilio to receive a 404 when it attempts to trigger the webhook, so use the same ngrok forwarding URL as before, appended with a non-existent route such as /broken, as depicted below:

Configure a broken voice webhook

Using your cell phone, make a phone call to your Twilio Phone Number, and press any key when prompted. You should hear that an error has occurred and right after you should see a new Slack Message sent from your “Twilio App” Slack App to the “Twilio Integration” channel detailing the error details.

Twilio error

To deploy your Flask App to production, you have a few options. You could use a service like PythonAnywhere, you could deploy to a Digital Ocean VPS, as described here, or to a service like Heroku, as described here. Once your application is deployed to a permanent URL, update the Twilio debugger webhook to forward errors to it.

Conclusion

In this project, you learned how to publish Twilio Debugger Events to a custom Slack Channel with Python and Flask. There are a variety of ways you could improve on this project, such as by tailoring the formatting for your use case, implementing an exponential backoff retry strategy, deploying the service to production, etc.

Jamie is an 18-year-old software developer located in Texas. He has particular interests in enterprise architecture (DDD/CQRS/ES), writing elegant and testable code, and Physics and Mathematics. He is currently working on a startup in the business automation and tech education space, and when not behind a computer, he enjoys reading and learning.