How to debug Twilio and Nextiva SMS webhooks
SMS webhooks are different from REST webhooks: form-encoded bodies, telco-specific signature schemes, and you can't just curl a fake message without a phone number. Here's a debugging workflow that doesn't cost $0.0075 per test.
Twilio sends SMS webhooks as application/x-www-form-urlencoded, not JSON. Nextiva (and Infobip, and most carriers) do similar. If you've only ever debugged REST webhooks, the first time a Twilio webhook shows up you'll look at it and wonder where the body went.
This walkthrough shows the actual body shape + how to debug the three common Twilio/Nextiva failure modes.
The body looks like this
Twilio inbound SMS:
Content-Type: application/x-www-form-urlencoded
MessageSid=SMabc123&AccountSid=ACabc&MessagingServiceSid=MGabc&From=%2B14155552671&To=%2B14155551234&Body=Hello%20World&NumMedia=0That's URL-encoded key=value pairs separated by &. Body=Hello%20World is the actual SMS content. Your handler needs to URL-decode it.
Nextiva and Infobip differ — some send JSON, some send form. Always inspect the raw request before writing the parser.
Setup: get a bin URL
Sign up at requestbin.net. Create a bin → copy URL → paste into:
- Twilio: Phone Numbers → Active numbers → A MESSAGE COMES IN, set to webhook URL with method POST
- Nextiva: SMS configuration → Inbound Webhook URL
- Infobip: API key + receive webhook config in MO endpoint settings
Failure mode 1: parsing the body
If your handler logs req.body as undefined or empty:
- Express: you need
express.urlencoded({ extended: false })middleware, NOT justexpress.json() - Next.js App Router:
const formData = await request.formData() - Hono / Bun:
const body = await c.req.parseBody()
Open the bin in RequestBin, look at the captured request body — if you see MessageSid=SM...&Body=..., your provider IS sending form data and you need form parsing.
Failure mode 2: signature validation
Twilio signs requests with X-Twilio-Signature. The signature is HMAC-SHA1 of fullUrl + sortedFormPairs using your Auth Token as the secret.
Common gotchas:
- URL must include query string AND no trailing slash drift. If Twilio sends to
https://yourapi.com/sms?account=abc, your handler must build the signature using exactly that URL, not/smsalone. - Form pairs must be sorted alphabetically by key.
- Behind Cloudflare/reverse proxy: the request hits your code at
http://localhost:3000/smsbut Twilio signed againsthttps://yourapi.com/sms. You need to reconstruct the original URL fromX-Forwarded-Proto+Hostheaders.
Use RequestBin's bin URL as the webhook target while debugging. The URL is stable, signature works against it without proxy munging, and you can see the exact bytes Twilio sent.
Failure mode 3: replay without burning credit
Each test SMS to Twilio costs $0.0075 plus the carrier fee. If you're debugging a handler that crashes 1 in 10 messages, that adds up.
Capture one real inbound message in the bin. Then click Replay — RequestBin re-fires the captured form-encoded body to your handler. Same shape, same headers, no SMS sent. Free on every plan.
Twilio signature verification example
import twilio from 'twilio';
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const params = req.body; // already form-parsed
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
signature,
url,
params,
);
if (!isValid) {
return res.status(403).send('Invalid signature');
}Going further
- Twilio Webhook Tester landing — sample headers + payload
- Infobip Webhook Tester — for the JSON-based carriers
- Replay docs