How to Handle Twilio Webhooks With Go and DigitalOcean Functions

October 02, 2024
Written by
Dotun Jolaoso
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Handle Twilio Webhooks With Go and DigitalOcean Functions

In this tutorial, we’ll explore how to receive and process Twilio webhooks using Go and DigitalOcean Functions.

Twilio webhooks provide a convenient way to receive real-time notifications about various events of your Twilio account. By leveraging DigitalOcean Functions, a serverless computing platform, you can handle these webhooks efficiently and perform custom actions based on the webhook data.

Technical requirements

To follow along, you’ll need the following:

Create a DigitalOcean Function

To start creating a DigitalOcean (DO) Function, you will use the doctl command line tool.

However, you need a personal access token to be able to use doctl. To generate one, head over to the API section of DigitalOcean's Dashboard. Click the Generate New Token button and fill in the form details, following the screenshot below. The account and function scopes are the custom scopes your token will need for this tutorial.

Before you can do anything with doctl, you need to initialize it to use your account. Do this by running the following command:

doctl auth init

Make sure to provide the access token you generated when you’re prompted to provide one.

You also 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 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 to it and start deploying Functions. Do this by running the following command:

doctl serverless namespaces create --label twilio-webhooks --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. Feel free to update the region to one close to where you live.

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

doctl serverless init --language go twilio

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

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

twilio/
├── packages
│   └── sample
│       └── hello
│           └── hello.go
└── 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 directory to twilio
  2. Rename the hello function directory to webhooks
  3. Rename the hello.go file to main.go

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

twilio/
├── packages
│   └── twilio
│       └── webhooks
│           └── main.go
└── project.yml

With the file and directory structure updated, open the project directory in your text editor or IDE of choice. Then, replace the contents of the project.yml file in the root of the project’s directory with the following:

parameters: {}
environment: {}
packages:
    - name: twilio
      shared: false
      environment:
        TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN}
        TWILIO_WEBHOOK_URL: ${TWILIO_WEBHOOK_URL}
      parameters: {}
      annotations: {}
      functions:
        - name: webhooks
          binary: false
          main: ""
          runtime: go:default
          webSecure: false
          parameters: {}
          environment: {}
          annotations: {}
          limits: {}

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

Install the dependencies

Next, we’ll install the only dependency the project will need, Twilio's Go helper library used to simplify communicating with Twilio’s API. In this case, we’ll be using the library to validate incoming Twilio webhook requests and construct TwiML responses.

First, navigate to the webhooks directory within the project and run the following code to initialize a Go module.

go mod init twilio/webhooks

Next, to install the dependencies, run the following commands:

go get github.com/twilio/twilio-go
go get github.com/twilio/twilio-go/twiml

This will install the dependency and create the entries needed in go.mod and go.sum. During deployment, the build process uses these files to download your third-party modules and build your function.

Build the Function

Replace the contents of the packages/twilio/webhooks/main.go file with the following code:

package main

import (
	"context"
	"net/http"
	"os"

	"github.com/twilio/twilio-go/client"
	"github.com/twilio/twilio-go/twiml"
)

type Response struct {
	StatusCode int             `json:"statusCode,omitempty"`
	Body       string          `json:"body,omitempty"`
	Headers    ResponseHeaders `json:"headers,omitempty"`
}

type ResponseHeaders struct {
	ContentType string `json:"Content-Type"`
}

const (
	OW_METHOD  = "__ow_method"
	OW_PATH    = "__ow_path"
	OW_HEADERS = "__ow_headers"
)

func Main(ctx context.Context, event map[string]interface{}) (*Response, error) {
	params := make(map[string]string)
	var signatureHeader string
	for key, value := range event {
		if key == OW_METHOD || key == OW_PATH || key == OW_HEADERS {
			continue
		}
		if key == "http" {
			h, ok := value.(map[string]interface{})
			if !ok {
				continue
			}
			headers, ok := h["headers"].(map[string]interface{})
			if !ok {
				continue
			}
			signatureHeader, _ = headers["x-twilio-signature"].(string)
			continue
		}
		s, ok := value.(string)
		if !ok {
			continue
		}
		params[key] = s
	}

	authToken := os.Getenv("TWILIO_AUTH_TOKEN")
	webhookUrl := os.Getenv("TWILIO_WEBHOOK_URL")
	validator := client.NewRequestValidator(authToken)
	isValid := validator.Validate(webhookUrl, params, signatureHeader)
	body := "Received webhook signature"
	if !isValid {
		body = "Invalid webhook signature"
	}
	message := &twiml.MessagingMessage{
		Body: body,
	}
	twimlResult, err := twiml.Messages([]twiml.Element{message})

	if err != nil {
		return &Response{StatusCode: http.StatusInternalServerError, Body: err.Error()}, nil
	}

	if !isValid {
		return &Response{StatusCode: http.StatusBadRequest, Body: twimlResult, Headers: ResponseHeaders{
			ContentType: "text/xml",
		}}, nil
	}

	return &Response{StatusCode: http.StatusOK, Body: twimlResult, Headers: ResponseHeaders{
		ContentType: "text/xml",
	}}, nil
}

We’ve defined a Main() function that serves as the entry point for our code and DigitalOcean Function. It processes incoming Twilio webhooks, validates the request signature, and responds with a TwiML message.

The Main() function is passed two parameters. The first parameter is a Go Context (context.Context) that represents the function’s execution context. The second parameter is a map that contains information about the HTTP request.

The incoming request from a Twilio webhook is of type application/x-www-form-url-encoded, so we created a map and populated it with all of the key/value pairs from the request. Currently, some additional legacy keys such as __ow_headers , __ow_method and __ow_path may be available in the event parameter. However, we don’t need the values of these keys, so they’re skipped.

To verify the request is authentic, you can use the RequestValidator struct from Twilio's Go helper library. The struct is initialized by passing in your Twilio Auth Token. The Validate() method on the struct is called passing in the webhook URL, which is obtained as an environment variable using os.Getenv(), the map 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.

Deploy the Function

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.

Head back to the root of the project’s directory and create a .env file. Then, update the file with the following:

TWILIO_AUTH_TOKEN=xxxx
TWILIO_WEBHOOK_URL=xxxx

Then, replace the TWILIO_AUTH_TOKEN with the actual value you copied 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, move to the project directory's parent directory and run the command below to deploy the function:

doctl serverless deploy twilio

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

doctl sls fn get twilio/webhooks --url

The command will output a URL similar to the one below:

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

Head back to the project’s directory, and update the TWILIO_WEBHOOK_URL field in the .env file with the URL printed to your terminal.

Next, change out of the project and then redeploy the project by running the deploy command, as follows:

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.

Configure your Twilio account

Twilio dashboard showing active numbers, details, and configuration options for messaging and voice.

Open your Twilio Console and head to Phone Numbers > Manage > Active Numbers. There, select your Twilio phone number.

Screenshot of a webhook URL configuration for message handling in a software settings page

You’ll be presented with a screen that shows you details about the phone number. Under the Messaging Configuration section: 

  • Set Configure with to "Webhook, TwiML Bin, Function, Studio Flow, Proxy Service"

  • Set "A message comes" in to "Webhook"

  • Paste the function URL in the "A message comes in" field 

  • Ensure the request method is set to "HTTP POST"

  • Click the Save configuration button at the bottom of the page to save the settings

This is the endpoint Twilio will send a request to whenever your Twilio phone number receives a message. 

Test that the application works

You can now send an SMS message to your Twilio phone number, and the DigitalOcean function should have been triggered.

Screenshot of a dashboard overview showing function insights and create trigger option.

You should receive a reply SMS with the body "Received webhook signature". And, if you look in the Overview section of your Function, in the DigitalOcean dashboard, you should see Activation Count set to 1 as in the screenshot above.

That's how to receive Twilio Webhooks using Go and DigitalOcean Functions

In this tutorial, you’ve learned how to create a DigitalOcean Function using Go to handle Twilio webhooks. We covered setting up the project, validating Twilio Webhook requests, and deploying the Function to DigitalOcean. 

This serverless approach provides a scalable and efficient way to process events from Twilio without managing server infrastructure. 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. You can find him at https://dotunj.dev/, https://github.com/Dotunj, and https://twitter.com/Dotunj_