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.

How to debug Twilio and Nextiva SMS webhooks

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=0

That'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 just express.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:

  1. 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 /sms alone.
  2. Form pairs must be sorted alphabetically by key.
  3. Behind Cloudflare/reverse proxy: the request hits your code at http://localhost:3000/sms but Twilio signed against https://yourapi.com/sms. You need to reconstruct the original URL from X-Forwarded-Proto + Host headers.

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

Start free →