Develop webhooks locally using Tailscale Funnel

April 05, 2023
Written by
Reviewed by

Develop webhooks locally using Tailscale Funnel

Webhooks are a way to be notified by an external service when an event has occurred. Instead of you sending an HTTP request to that service, the service sends an HTTP request to your public web service. This way, you can respond to the event in real-time. Webhooks are also a common way to integrate with Twilio's products. For example, when your Twilio phone number receives a text message or phone call, Twilio sends an HTTP request (the webhook) to your service with the details. Your service then responds with instructions that indicate how Twilio should respond to the event. Here's a diagram of what this exchange looks like:

One common challenge with webhooks is that they can only call web services that are publicly available on the internet, but when you are initially developing software, you are typically doing so on your local machine, which by default is not reachable via the internet. So how do you develop and test webhooks on your local machine? The answer: tunnels. In this post, you'll learn how to use Tailscale Funnel, a tunnel service by Tailscale in beta, to make your local web server publicly accessible on the internet. Then you'll learn how to use Funnel to respond to a Twilio webhook to respond to a phone call.

Local tunnels

You can create a tunnel between your local machine and a tunnel service to expose local ports to the public internet. There are many tools that can help you do this, and today you're going to learn about Tailscale Funnel.


You will need the following things to follow along:

  • A Linux, macOS, or Windows machine
  • Your development stack of choice (Node.js, PHP, .NET, Java, Python, etc.)
  • A Tailscale network also known as a tailnet with your development machine joined to the network. Follow this Tailscale quickstart to do so.
  • Optional: A Twilio account (Try Twilio for free)

Set up a local web server

You can tunnel a local port at any time, but you won't see any results if nothing is listening to that port on your machine. In this section, you'll set up a local web server for serving static files.

Usually, the response for a webhook is created programmatically, but for the purposes of this demo, you'll use static files.

Open your preferred shell, create a folder, and navigate to it:

mkdir MyStaticSite
cd MyStaticSite

Create a new file index.html in the MyStaticSite folder with the following content:

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World!</title>
    Hello World!

You'll need to run a static web server to serve this HTML file. There are many static servers for every development stack out there. Pick your preferred stack below and follow the instructions to serve the current folder at http://localhost:8080/.

Node.js: Run the following command to execute the http-server NPM package and run it:

npx http-server . --port 8080

PHP: You can use the built-in PHP web server by running the following command:

php -S localhost:8080

.NET: You can use the dotnet-serve tool to run a static web server. Install the tool as a global .NET tool:

dotnet tool install --global dotnet-serve

Run the tool to serve the current directory:

dotnet serve --port 8080

Python 3:

python -m http.server 8080

Check out this list of static web server commands if your preferred programming language is not listed above.

Leave the static server running and open a new shell for the upcoming commands.

Tunnel your local web server using Tailscale Funnel

You can use the ​​Tailscale Funnel to tunnel your local ports to the public internet.

Before you can use Tailscale Funnel, you'll need to:

  1. Give access to yourself or whoever needs access to this feature
  2. Enable HTTPS

First, go to the Access Controls in your Tailscale admin page, and add the highlighted JSON (line 29 - 34) to the file and hit Save.

// Example/default ACLs for unrestricted connections.
        // Declare static groups of users beyond those in the identity service.
        "groups": {
                "group:example": ["", ""],

        // Declare convenient hostname aliases to use in place of IP addresses.
        "hosts": {
                "example-host-1": "",

        // Access control lists.
        "acls": [
                // Match absolutely everything.
                // Comment this section out if you want to define specific restrictions.
                {"action": "accept", "src": ["*"], "dst": ["*:*"]},
        "ssh": [
                // Allow all users to SSH into their own devices in check mode.
                // Comment this section out if you want to define specific restrictions.
                        "action": "check",
                        "src":    ["autogroup:members"],
                        "dst":    ["autogroup:self"],
                        "users":  ["autogroup:nonroot", "root"],
        "nodeAttrs": [
                        "target": ["autogroup:members"],
                        "attr":   ["funnel"],

This will give access to Funnel to the autogroup:members group, which are the direct members of your tailnet.

Next, click on the DNS tab and click the Enable HTTPS… button. 

Now you should be able to start using Funnel.

Open your preferred shell and run the following commands:

tailscale serve https / http://localhost:8080
tailscale funnel 443 on

The tailscale serve command serves your web server running on http://localhost:8080 over HTTPS (port 443) on the current node (your machine) of your tailnet. At this point, your web server is accessible to others on your telnet, but not to the internet.

The tailscale funnel command creates the tunnel between your locally running server, being served on your tailnet, to the internet over port 443.

Run the following command to see the Funnel status:

tailscale funnel status

The output should look something like:

# Funnel on:
#     - (Funnel on)
|-- / proxy

Copy the URL next to "Funnel on" which is the public URL where you can reach your web server.

Go to the URL using a web browser. You'll see the HTML file that is served from your local web server is now publicly served at the address Tailscale gave you.

You can use this public address and configure it as your webhook address with the external services you want to integrate with.

Use Tailscale Funnel with Twilio webhooks

You can also use these tunnels to respond to Twilio webhooks. In this section, you'll learn how to respond to a phone call using a static XML file.

Create a new file call.xml in the MyStaticSite folder:

<?xml version="1.0" encoding="UTF-8"?>
  <Say voice="alice">Twilio received these instructions from your local machine via Tailscale Funnel.</Say>
  <Pause length="1" />
  <Say voice="alice">Great job!</Say>

This XML file uses TwiML, also known as the Twilio Markup Language. Using TwiML, you can provide instructions on how Twilio should respond to an incoming call or SMS. The TwiML above uses two TwiML Voice verbs: Say and Pause. Say converts text to speech, and the voice attribute specifies which voice to use. Pause will wait the specified amount of time, which is one second in this case, and then move on to the next verb.

Make sure your static web server is still running, and browse to the XML file using the publicly tunneled URL: [funnel-url]/call.xml

The XML should be returned to your browser. Take note of this URL.

If you don't already have a Twilio account, you can get a free Twilio account here.

Next, you'll need a Twilio phone number. Go and buy a new phone number from Twilio. The cost of the phone number will be applied to your free promotional credit if you're using a trial account.

Use the left side navigation to navigate to Phone Numbers > Manage > Active numbers. Click on your Twilio phone number to navigate to its settings.

Find the A CALL COMES IN fields under the Voice & Fax section. Change the text field to your public tunnel URL with the /call.xml path, and change the HTTP verb to HTTP GET.        

Click Save to submit the form.

To verify the call is working as expected, call your Twilio phone number using your personal phone. You should hear "Twilio received these instructions from your local machine via Tailscale Funnel. Great job!".

Using a static file is sufficient to demonstrate local tunneling, but you can also use the same techniques in combination with any web server technology to process and respond to the HTTP request.

If you're using HTTPS instead of HTTP for local development, you can still use Tailscale Funnel, but need to update the http:// scheme to https://, like tailscale serve https / https://localhost:8080.
If you are using HTTPS locally, there's a good chance you're using self-signed certificates, in which case you'll need to use https+insecure:// as the scheme, as Funnel will parse this scheme, so the command would look like tailscale serve https /https+insecure://localhost:8080.

Developing webhooks locally with Tailscale Funnel

Webhooks are a great way to be notified by an external service when an event has occurred. The external service makes an HTTP request to your public web service and provides the details of the event in the body of the request. You can use local tunneling to make your local port accessible on the internet using a tunnel service like Tailscale Funnel. Using Tailscale Funnel, you can tunnel your local port to the public URL provided by Tailscale. Check out the Tailscale Funnel docs to learn more about its capabilities and limitations.

Many Twilio products have webhook capabilities. Learn how you can infuse communication technology into your application in the Twilio Docs, and learn how to get started quickly with Twilio's SDKs.

Niels Swimberghe is a Belgian American software engineer and technical content creator at Twilio, and a Microsoft MVP in Developer Technologies. Get in touch with Niels on Twitter @RealSwimburger and follow Niels’ personal blog on .NET, Azure, and web development at