Test Twilio Webhooks Locally
Twilio is unusual among providers here: voice and SMS webhooks must be HTTPS, and Twilio validates X-Twilio-Signature on every callback. This guide assumes you already picked tunnel vs CLI in Test Webhooks Locally.
Try it in 60 seconds
Twilio requires a public HTTPS URL for voice and SMS webhooks. No account signup on the tunnel side:
npm run dev
npx localtolink --port 3000
Set your Twilio phone number's webhook to https://k7m2x9p4q1.localtolink.com/twilio/voice. Call the number. Your local handler receives the POST and returns TwiML. Twilio Monitor shows delivery status if something fails.
How Twilio webhooks reach your app
Twilio sends HTTP POST requests to your server when calls arrive, SMS messages come in, call status changes, or TwiML actions complete. Your local dev server on localhost cannot receive these — Twilio's servers need a public HTTPS URL.
Twilio documentation often references tunnel tools generically — any HTTPS tunnel that forwards POST requests works the same way. LocalToLink provides a public URL without account signup.
Setup
Terminal 1 — your Twilio webhook handler:
npm run dev
Implement routes for the TwiML and status callbacks you configure in Twilio Console.
Terminal 2 — tunnel:
npx localtolink --port 3000
Configure Twilio
In Twilio Console, set webhook URLs for the relevant resource:
| Resource | Webhook field |
|---|---|
| Phone number (SMS) | "A message comes in" |
| Phone number (Voice) | "A call comes in" |
| TwiML App | Voice/SMS request URL |
| Status callback | Status callback URL on calls/messages |
Example voice webhook URL:
https://k7m2x9p4q1.localtolink.com/twilio/voice
Example SMS webhook URL:
https://k7m2x9p4q1.localtolink.com/twilio/sms
Replace k7m2x9p4q1 with the subdomain LocalToLink assigns.
Handler example (Express)
app.post("/twilio/voice", (req, res) => {
const twiml = new twilio.twiml.VoiceResponse();
twiml.say("Hello from your local dev server");
res.type("text/xml");
res.send(twiml.toString());
});
Return valid TwiML XML for voice endpoints. Return 200 with an empty body or TwiML for SMS.
Validate Twilio requests
Twilio signs requests with X-Twilio-Signature. Use the official SDK to validate:
import twilio from "twilio";
const valid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
req.headers["x-twilio-signature"],
fullUrl, // Must be the full tunnel URL, not localhost
req.body
);
if (!valid) {
return res.status(403).send("Forbidden");
}
Critical: Use the full public tunnel URL when validating signatures, not http://localhost:3000/.... Twilio computes the signature against the URL it POSTed to.
Testing the flow
- Configure webhook URL in Twilio Console
- Call or text your Twilio phone number
- Watch your local server logs for the incoming POST
- Check Twilio Console → Monitor → Logs for delivery status and errors
Twilio shows HTTP response codes and error messages when webhooks fail.
Twilio Media Streams (WebSocket)
If you use Twilio Media Streams for real-time audio, Twilio connects to your server via WebSocket. LocalToLink supports WebSocket passthrough — configure your stream URL as:
wss://k7m2x9p4q1.localtolink.com/twilio/stream
See WebSocket Through a Tunnel for details.
Verify the tunnel before Twilio
Before saving Console URLs, confirm the public route responds:
curl -sS -o /dev/null -w '%{http_code}\n' https://k7m2x9p4q1.localtolink.com/voice/inbound
Twilio's servers use non-browser user agents, so they skip the visitor interstitial when that policy is enabled. Your manual browser test may show Opening a shared link once — that is normal and does not affect webhook delivery.
Important notes
- Twilio POSTs are server-to-server; for human checks in a browser, open the link once and continue through any safety prompt
- Session expires after 2 hours — update webhook URLs when restarting
- Update Console URLs when subdomain changes
- LocalToLink has no request inspector — use Twilio Monitor logs
- Twilio retries on failure with exponential backoff
Troubleshooting
| Problem | Fix |
|---|---|
| Signature validation fails | Use full tunnel URL in validateRequest, not localhost |
| Twilio shows 11200 error | Tunnel unreachable — confirm it is running |
| Session ended | Restart tunnel and update Console URLs |
| TwiML not executing | Confirm response Content-Type is text/xml |
| Media Streams disconnect | Check WebSocket endpoint uses wss:// tunnel URL |
FAQ
Can I use Twilio's test credentials?
Yes. Test credentials work with tunnel URLs the same way as live credentials for development.
Does Twilio support localhost URLs?
No. Twilio requires publicly reachable HTTPS URLs for webhooks.
How do I debug failed deliveries?
Use Twilio Monitor logs for HTTP status and response body. Add temporary logging in your handler for request payloads.
See Test Webhooks Locally for the general webhook hub.
Try it now
Start a tunnel from your project directory — no account required.
$ npx --yes localtolink@latest