How to Validate Twilio Event Streams Webhooks in Go

October 01, 2025
Written by

How to Validate Twilio Event Streams Webhooks in Go

One of Twilio's stand out features is Event Streams Webhooks! These let your application know when specific events happen, such as a shortened link was clicked, an SMS was successfully delivered, read, or sent, or a Verify verification was approved.

The webhooks include comprehensive details of the event, such as the body of an incoming message, the phone or WhatsApp number it was sent from, and the event's name. With that information, your application can become extremely flexible and powerful.

However, your applications should validate webhooks just like any other request, so that you know it came from Twilio — not a malicious actor.

Gladly, Twilio makes this almost trivial as each webhook request is signed. This signature, in combination with your account's Auth Token, the body of the webhook, and the request URL, can be used to verify that the webhook came from Twilio and that it has not been tampered with.

In this short tutorial, you're going to learn how to do just that in Go, using Twilio's Go Helper Library.

Let's begin.

Prerequisites

To follow along with this tutorial, you'll need the following:

How the code will work

The code will be pretty small; a web app with one route (/webhook). This route will receive webhook requests from Twilio. When requests are received, the app will validate them. If the request is valid, it will write "Valid signature" to the app's output. If not, "Invalid signature" will be written instead.

Create the core of the project

Wherever you store your Go projects, run the commands below to create a new project directory and change into it.

mkdir -p validate-webhook-signature
cd validate-webhook-signature

If you're using Microsoft Windows, don't worry about the -p option, as it's not required.

Add the required packages

The next thing to do is to install the two required packages. To install them, run the following command:

go mod init
go get github.com/joho/godotenv github.com/twilio/twilio-go
If you're using Linux, a BSD, or macOS with a shell that supports Glob expressions and want an even shorter command for installing the Go modules, run go get github.com/{joho/godotenv,twilio/twilio-go} instead.

Configure the required environment variables

You next need to set two environment variables which the app needs so that it can validate the webhook; these are NGROK_URL and TWILIO_AUTH_TOKEN.

To set them, create a new file named .env in the project's top-level directory. Then, in that file paste the configuration below.

NGROK_URL=<NGROK_URL>
TWILIO_AUTH_TOKEN=<TWILIO_AUTH_TOKEN>
Screenshot

Next, in the Account Dashboard of your Twilio Console, copy your Auth Token and paste it into .env in place of <TWILIO_AUTH_TOKEN>.

Write the Go code

Start by creating a file named main.go in the project's top-level (root) directory. Then, in that file, add the following code:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/joho/godotenv"
	"github.com/twilio/twilio-go/client"
)

func validateWebhook(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		log.Printf("Could not retrieve JSON request body")
		return
	}

	requestValidator := client.NewRequestValidator(os.Getenv("TWILIO_AUTH_TOKEN"))
	requestedUrl := strings.Join(
		[]string{
			os.Getenv("NGROK_URL"),
			r.URL.Query().Encode(),
		},
		"/webhook?",
	)

	twilioSignature := r.Header.Get("x-twilio-signature")
	isValid := requestValidator.ValidateBody(
		requestedUrl,
		body,
		twilioSignature,
	)

	if !isValid {
		fmt.Println("Valid signature")
		return
	}
	fmt.Println("Invalid signature")
}

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/webhook", validateWebhook)
	log.Print("Starting server on :4000")
	err = http.ListenAndServe(":4000", mux)
	log.Fatal(err)
}

The code defines a function named validateWebhook(), which handles requests to the application's default route. The function starts off by retrieving the request's body as a byte array. If this fails, it prints a log message so that we know something went wrong and returns from the function.

Otherwise, it:

  • Initialises a client.RequestValidator object with your Twilio Auth Token to simplify validating the request
  • Retrieves the requested URL (built from a combination of the base URL, defined in NGROK_URL (which will be set shortly), the route's path (/webhook), and the request's query string)
  • Retrieves the request signature contained in the "X-Twilio-Signature" header

It passes these three variables to the call to ValidateBody() to determine if the request is valid. If the request is valid, it writes "Valid signature" to the application's output. Otherwise, it writes "Invalid signature".

Make the application publicly accessible

You next need to expose the application to the public internet, so that Twilio can send webhook requests to it. To do that, we're going to use ngrok, by running the command below.

Run the command below to create a secure tunnel to port 4000; the application doesn't need to be running for this to work.

ngrok http 4000
If you're not familiar with ngrok, it's a quick and easy way to create secure tunnels to applications running in local development environments.

In the terminal's output, you'll see a Forwarding URL. Copy that and paste it into .env in place of <NGROK_URL>.

Ngrok status showing connection details and an available update for version 3.1.4.1.

Create a Sink

The next step is to create a Sink. A sink is the destination where events will be sent. Conveniently, they also allow sending a test event to validate the integration works as expected.

Interface for managing sinks and subscriptions with Event Streams on Twilio.

To create one, in the Twilio Console, navigate to Explore Products > Developer Tools (right near the bottom of the page) > Event Streams. There, click "Create new sink" to start the process.

Interface for creating a new event sink with options for Amazon, Webhook, and Segment sinks.

Then, set Sink description to "Validate Webhook Sink", set sink type to "Webhook", and click "Next step".

Interface for creating a new webhook sink with URL and method options on a management platform.

After that, set Destination to the Forwarding URL printed by ngrok to the terminal, with "/webhook" at the end, leave Method set to "POST", and click Finish. Then, in the confirmation popup that appears, click "View Sink Details", taking you to the sink's properties page.

Test that the application works

Right, let's check that the code works as expected. To do that, first start the application listening on port 4000 by running the following command in your terminal.

go run main.go

Now, back in the Twilio Console, on the Sink resource's properties page, click "Send test event".

Screenshot of the Validate Webhook Sink settings interface showing active status and description input.

Back in your terminal, you should see output similar to the following printed to the terminal if the webhook was valid:

Valid signature

That's how to how to validate Twilio Event Streams webhooks in Go

There's a (little) bit to do to get it all set up and running. However, after it's done, you can now validate that the webhook data you receive truly is from Twilio. Remember, no input should ever be trusted unconditionally.

Feel free to use the repository on GitLab, or to contribute to it.

Matthew Setter is a PHP and Go editor in the Twilio Voices team and a PHP and Go developer. He’s also the author of Mezzio Essentials and Deploy With Docker Compose. You can find him at msetter[at]twilio.com and on LinkedIn and GitHub.

Quality control icons created by Freepik on Flaticon.