← All guides
TwilioJuly 12, 20266 min read

Test Twilio Webhooks on Localhost Without Deploying

Receive, route, and transform Twilio webhooks on your twilio webhook localhost setup. Forward SMS and status callbacks to your dev machine in minutes.

Twilio webhooks are the backbone of any SMS or voice app you build: an inbound message arrives, Twilio POSTs it to your server, and your code decides what happens next. Status callbacks tell you when a message is queued, sent, delivered, or failed. The problem is that Twilio needs a public HTTPS URL to reach you — and your dev machine is not public.

The usual workarounds are painful. You deploy half-finished code to a staging box just to see one payload. You paste a tunnel URL into the Console, then paste a new one an hour later when the tunnel restarts. You add console.log everywhere because you can't inspect what Twilio actually sent. None of this is real local development.

This guide sets up a stable public endpoint with Relayers that forwards Twilio's exact request — body and headers untouched — straight to your localhost. Your signature verification keeps working, your breakpoints fire, and the URL never changes.

What you'll build

Twilio only ever sees one stable public URL. Relayers handles the tunnel, and your app runs exactly where it already runs.

Prerequisites

1Create an endpoint in Relayers

Open the Endpoints view and create a new endpoint. Relayers gives you a public ingest URL that looks like this:

https://api.relayers.app/v1/webhooks/wep_9f2a1c7b

Click Copy URL to grab it. Before wiring up Twilio, hit Send test to fire a sample payload through the endpoint so you can confirm the plumbing works end to end.

2Point Twilio at your endpoint

You can configure Twilio at the phone-number level or on a Messaging Service.

On a phone number: Console → Phone NumbersManage → your number → Messaging (or Voice). Find the "A message comes in" setting, choose Webhook, paste your Relayers URL, and set the method to HTTP POST:

A MESSAGE COMES IN
Webhook:  https://api.relayers.app/v1/webhooks/wep_9f2a1c7b
Method:   HTTP POST

On a Messaging Service: Console → MessagingServices → your service → Integration, then set the inbound request URL to the same Relayers endpoint.

Status callbacks (delivery receipts): If you want delivery events (queued, sent, delivered, failed), set a status callback URL — either on the Messaging Service or per-message via the StatusCallback parameter. You can point it at the same Relayers endpoint and split the traffic later with a rule.

Two things to keep in mind about Twilio's requests:

Relayers forwards both the raw body and every header verbatim, so nothing about this changes downstream.

3Forward events to localhost

Log in and start listening with the CLI:

relayers login
relayers listen --forward localhost:3000

That connects a tunnel from your Relayers endpoint to your local server. Send yourself a text message, and the inbound SMS webhook flows Twilio → Relayers → localhost:3000. Prefer a GUI? The desktop app does the same thing with a toggle.

Every request also lands in the Events view with a full payload inspector, so you can see exactly what Twilio sent — form fields, headers, and all.

4Route and filter with JQ

Even though Twilio sends form-encoded data, Relayers parses those fields so your JQ filters operate on structured values. Open the Rules view and add rules — remember that the first match wins, and you can reorder them.

Route inbound SMS whose body contains a keyword (say, STOP) to your local handler:

.Body | ascii_downcase | test("stop")

Send that rule's destination to your tunnel → localhost:3000.

Split delivery status callbacks off to a different destination by matching on the presence of MessageStatus:

.MessageStatus != null

Point that rule at a separate local port or a public URL — whatever handles your delivery-receipt logic. Because rules evaluate top-down, put the more specific status-callback rule above the catch-all inbound rule.

5Transform the payload

Sometimes your local handler wants a clean, minimal shape instead of Twilio's full form dump. Open the Transformations view, pick an engine (jq, jsonata, javascript, or go_template), and reduce the payload:

{ from: .From, body: .Body }

Use the built-in test playground to paste a sample Twilio payload and see the output before you save. Now your app receives a tidy { "from": "+15551234567", "body": "hello" } instead of a dozen form fields.

A note on signatures

Twilio signs each request with X-Twilio-Signature. Relayers passes that header through untouched, so you can validate it locally with Twilio's SDK exactly as you would in production.

One critical detail: the signature is an HMAC over the full URL Twilio called plus the sorted parameters. Twilio called your public Relayers URL, not localhost. So when you verify, hash against your Relayers endpoint URL (https://api.relayers.app/v1/webhooks/wep_...) — not your local address. Feed that URL into the validator and the signature checks out.

Going to production

When you ship, keep the same Relayers endpoint and simply add a rule whose destination is your production URL instead of the tunnel. Your Twilio configuration never has to change — the public URL is stable for good.

Use Import/Export to move your endpoint, rules, and transformations between environments as a JSON bundle, so staging and production stay in sync without hand-editing.

Troubleshooting

Wrap-up

With one stable Relayers endpoint you can develop Twilio SMS and voice integrations entirely on your own machine — inbound messages and status callbacks forwarded to localhost, JQ rules to route them, transformations to reshape them, and signature verification that still works because the original request is preserved.

Ready to test your first Twilio webhook locally? Get started at relayers.app and download the app to open your first tunnel.