How to Test Twilio SMS and Voice Webhooks
Point a Twilio number at a capture URL, read the form-encoded parameters, reply with TwiML without writing a server, and validate X-Twilio-Signature with the helper libraries.
Updated:
When someone texts or calls your Twilio number, Twilio sends an HTTP request to your webhook and expects instructions back in TwiML, an XML format. That makes Twilio different from most webhook providers: the response content matters, not just the status code. This guide shows how to see exactly what Twilio sends, return a valid TwiML reply straight from a capture URL, and validate the signature in your own code.
Step 1: Create a capture URL
Open Webhook Inspector and create a URL, for example:
https://webhook.voronyuk.com/abcd23efgh/twilio/sms
Any sub-path is captured, so you can use .../twilio/sms for messages and .../twilio/voice for calls on the same URL.
Step 2: Set a TwiML response
The default response, {"ok":true,"request_id":N}, isn't TwiML. Twilio needs XML to know what to do. In the URL's custom response settings, set:
- Status:
200 - Header:
Content-Type: text/xml - Body:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Hi!</Message>
</Response>
Now every incoming SMS gets "Hi!" back, with no server of your own. For voice, use a body like:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>Hello from the webhook test.</Say>
</Response>
The response is static, so it's the same for every request. That's enough to confirm the number is wired up and to read the incoming parameters. Forwarding (Step 5) runs your code too, but Twilio still receives this static TwiML, so dynamic replies need a different approach — see below.
Step 3: Configure the phone number
- In the Twilio Console, open your phone numbers and select the active number.
- In the messaging configuration, find A message comes in, choose Webhook, paste
https://webhook.voronyuk.com/abcd23efgh/twilio/smsand select HTTP POST. - For calls, set A call comes in the same way with
.../twilio/voice. - Save.
If the number belongs to a Messaging Service, incoming messages may be handled by the service's own webhook settings instead, so check there if nothing arrives.
Step 4: Read the parameters
Send an SMS to the number. The request appears live in the inspector. Twilio sends application/x-www-form-urlencoded data, which the inspector decodes into readable fields:
| Parameter | Example | Meaning |
|---|---|---|
MessageSid |
SM... |
Unique ID of the message |
AccountSid |
AC... |
Your account |
From |
+380501234567 |
Sender's number |
To |
+15005550006 |
Your Twilio number |
Body |
hello |
Message text |
NumMedia |
0 |
Number of attached media files |
Voice requests include CallSid, From, To, CallStatus and Direction. You'll also see the X-Twilio-Signature header on every request.
How X-Twilio-Signature works
Twilio signs each request with your account Auth Token:
- Take the full URL Twilio requested, including any query string.
- If the request is a POST, sort the POST parameters alphabetically by name and append each name and value to the URL, with no separators.
- Compute HMAC-SHA1 of that string with your Auth Token as the key.
- Base64-encode the result.
You rarely need to implement this. The official helper libraries do it, including the constant-time comparison.
The URL is the detail that trips people up. Twilio signed the URL it called, which is the public capture URL, not http://localhost:3000/sms. When validating forwarded requests, pass the public URL.
Node.js
const express = require('express');
const twilio = require('twilio');
const AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
const PUBLIC_URL = 'https://webhook.voronyuk.com/abcd23efgh/twilio/sms';
const app = express();
app.post('/sms', express.urlencoded({ extended: false }), (req, res) => {
const signature = req.get('X-Twilio-Signature');
if (!twilio.validateRequest(AUTH_TOKEN, signature, PUBLIC_URL, req.body)) {
return res.status(403).send('Invalid signature');
}
const twiml = new twilio.twiml.MessagingResponse();
twiml.message(`You said: ${req.body.Body}`);
res.type('text/xml').send(twiml.toString());
});
app.listen(3000);
Python
import os
from flask import Flask, Response, abort, request
from twilio.request_validator import RequestValidator
from twilio.twiml.messaging_response import MessagingResponse
app = Flask(__name__)
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
PUBLIC_URL = "https://webhook.voronyuk.com/abcd23efgh/twilio/sms"
@app.post("/sms")
def sms():
signature = request.headers.get("X-Twilio-Signature", "")
if not validator.validate(PUBLIC_URL, request.form, signature):
abort(403)
reply = MessagingResponse()
reply.message(f"You said: {request.form.get('Body', '')}")
return Response(str(reply), mimetype="text/xml")
if __name__ == "__main__":
app.run(port=3000)
Step 5: Forward to your app
In the Forwarding panel, set the target to http://localhost:3000. The captured sub-path is appended, so .../abcd23efgh/twilio/sms goes to http://localhost:3000/sms. Use the CLI agent:
WI_TOKEN='<token>' inspector listen https://webhook.voronyuk.com/abcd23efgh/twilio --to localhost:3000
Copy the exact command with your token from the panel. The browser relay works too if your server allows CORS. See testing webhooks locally for the differences.
Forwarding is one-way: your app receives every request, but Twilio always gets the inspector's response, never your app's TwiML. To reply dynamically while testing, send the answer from your app through Twilio's Messages REST API instead of TwiML. When the logic is ready, point the number at your app's own public URL and return TwiML directly.
While you iterate, replay any captured message from the Twilio webhook tester instead of texting the number again. Replays are a good way to exercise parsing logic; signature validation will still pass only if the URL and parameters are unchanged.
Related: Telegram bot webhooks and Slack Events API.
FAQ
How do I test a Twilio SMS webhook without a server?
Set the number's "A message comes in" webhook to a capture URL, and give that URL a custom response with Content-Type: text/xml and a TwiML body. You'll see every incoming message and the sender gets a reply.
Why does Twilio say my webhook response is invalid?
Twilio expects TwiML. A JSON body or a wrong Content-Type won't work. Return XML with a <Response> root and Content-Type: text/xml.
Why does X-Twilio-Signature validation fail?
Usually the URL doesn't match the one Twilio called, for example you used the localhost URL after forwarding, or dropped the query string. Validate with the exact public URL and the original POST parameters.
What parameters does Twilio send for an incoming SMS?
Among others: MessageSid, AccountSid, From, To, Body and NumMedia, sent as form-encoded fields.