LLOQU
ProductPricingBlogDocsLogin

On this page

Why is my Twilio webhook not working?Step one: is Twilio calling your server at all?What the error code in the Debugger tells youWhen Twilio can't reach your URLThe two places a voice webhook URL livesYour server answered, and Twilio rejected the answerSignature validation is rejecting real Twilio requestsDebugging webhooks locally with the ngrok inspectorFAQThe webhook you don't have to run
All articles
TwilioWebhooksErrors

Twilio Webhook Not Working: How to Diagnose and Fix It

Twilio webhook not working? Find out whether Twilio is reaching your server at all, then fix the causes behind almost every webhook failure.

Alloqui TeamAug 13, 202610 min read
On this page
Why is my Twilio webhook not working?Step one: is Twilio calling your server at all?What the error code in the Debugger tells youWhen Twilio can't reach your URLThe two places a voice webhook URL livesYour server answered, and Twilio rejected the answerSignature validation is rejecting real Twilio requestsDebugging webhooks locally with the ngrok inspectorFAQThe webhook you don't have to run
Back to all articles
LLOQUAlloqui

Product

  • Features
  • Pricing
  • Blog

Developers

  • Documentation
  • @alloqui/dialer on npm

Company

  • Contact

Legal

  • Privacy
  • Terms

© 2026 Alloqui

A Twilio webhook that isn't working has exactly two shapes: either Twilio never reached your server, or Twilio reached it and rejected what came back. The Twilio Debugger tells you which one you have in about thirty seconds, and the two paths have nothing in common — one is a networking problem, the other is a code problem. This guide runs the diagnosis first, then works through the causes in the order they actually happen: unreachable URLs, a URL configured in the wrong place, a server that errors or stalls, a response Twilio can't parse, and signature validation quietly rejecting legitimate requests.

Why is my Twilio webhook not working?

Twilio makes an ordinary HTTP request to a URL you configured, and expects a fast 2xx response with valid TwiML. A webhook "not working" means one link in that chain broke: the URL doesn't resolve or isn't publicly reachable, the URL you edited isn't the URL Twilio is actually calling, your server returns a non-2xx status, your server takes longer than Twilio's 15-second read timeout, or your response body isn't parseable XML. A sixth cause looks identical from the outside but is self-inflicted: your own X-Twilio-Signature check returning 403 on requests that genuinely came from Twilio.

Every one of those leaves a record in the Twilio Console. Start there, not in your application logs.

Step one: is Twilio calling your server at all?

Open Monitor → Logs → Errors in the Twilio Console and find an entry matching the timestamp of a failed call or message. This is the fork in the road, and it decides everything you do next.

The Debugger entry carries a webhook subdocument with both halves of the exchange: the request Twilio sent (method, URL, headers, parameters) and the response it got back (status code, headers, and the response body itself). If you see the exact URL Twilio requested, you have confirmation that Twilio tried — and the response fields tell you what your server said. If there is no entry at all, Twilio never had a reason to call you, which points at configuration rather than at your code.

Two things worth checking in that request block before you go anywhere else. First, the URL: read it character by character against the URL you think you configured, including the scheme, the path, and any trailing slash. Second, the method — Twilio sends GET or POST for voice depending on what you set, and a handler registered only for POST returns 404 or 405 to a GET.

You can also have these events pushed to you rather than pulled: Twilio's debugging events webhook posts the same payload to a URL of your choice as errors happen, which is the only practical way to notice webhook failures in production before a customer does.

What the error code in the Debugger tells you

The Debugger entry carries a numeric code, and it splits the problem cleanly along the same fault line as the diagnosis above:

CodeWhat Twilio is telling youWhere to look
11200HTTP retrieval failure — Twilio sent the request and got back a non-2xx response, a connection failure, or something it couldn't parseThe network and status-code sections below
12100Document parse failure — Twilio reached your server, got a response, and couldn't read it as XMLThe TwiML and Content-Type section below

Both codes get their own dedicated guides in a later batch of this series. For now, treat 11200 as "the request failed" and 12100 as "the request succeeded and the answer was malformed", and use the sections below accordingly.

When Twilio can't reach your URL

Twilio calls your webhook from its own infrastructure over the public internet, which rules out a whole category of URLs that work perfectly from your laptop.

Anything private is unreachable. localhost, 127.0.0.1, 192.168.x.x, and internal hostnames all fail, and Twilio lists "URL resolving to private/localhost IP addresses" among the documented causes of 11200. Local development needs a tunnel.

Tunnel URLs rotate. An ngrok URL from a free-tier session is dead the moment the agent restarts, and the Twilio config still points at it. This single fact is behind a large share of "it worked yesterday" reports. Twilio's own getting-started guide also warns that "ngrok or other HTTP tunnels may be blocked by your internal network's firewalls" — a tunnel that opens fine can still be unreachable from outside your office.

Self-signed certificates do not work. Twilio's documentation is unambiguous: "Twilio webhooks do not work on HTTPS endpoints with self-signed SSL certificates." A staging box with a self-signed cert will fail every webhook while looking healthy in a browser that you clicked through the warning on.

Firewalls, WAFs, and rate limiters block silently. These are the hardest to spot because your server logs show nothing — the request never arrived. Check the layer in front of your application, not the application. Note that you cannot solve this by allowlisting Twilio's IPs (see the FAQ below).

Redirects have to land somewhere valid. Twilio follows a 302, but a redirect to an unreachable destination, or to a login page, counts as a failure. An HTTP-to-HTTPS redirect at your load balancer is fine; a redirect into an SSO flow is not.

The two places a voice webhook URL lives

This is the configuration mistake that survives the longest, because the URL you keep editing is real, saved, and completely ignored.

A phone number's A call comes in setting can point at one of two things: a Webhook (a URL you type directly on the number) or a TwiML App (a saved object with its own Voice Request URL and method). If the number is set to TwiML App, the URL field on the number does nothing — Twilio reads the VoiceUrl off the application instead. People edit the number, test, see no change, and conclude the webhook is broken.

Browser calling inverts the problem. Outbound calls from the Voice SDK never touch the phone number's configuration at all: they route through whichever TwiML App SID your access token carries in outgoingApplicationSid. A token minted against a deleted or stale application produces calls that fail at setup, which the browser typically reports as a generic error 31000 or, when the far end rejects the resulting TwiML, as error 31002 connection declined.

Check all four of these in Console → Voice → TwiML Apps and on the number itself:

  1. Which mode the number is in — Webhook or TwiML App.
  2. The Voice Request URL on whichever object is actually in play.
  3. The HTTP method next to it, matched to what your route accepts.
  4. The application SID your token is minted with, matched to the app you just edited.

Confirm the fallback URLs too, or deliberately leave them empty. A VoiceFallbackUrl pointing at a long-dead endpoint turns one failure into two.

Your server answered, and Twilio rejected the answer

Once the Debugger shows a response, the problem is in what your handler returns. Twilio wants an HTTP 2xx, a Content-Type of text/xml or application/xml, and valid TwiML in the body — fast.

Fast is a specific number. Twilio publishes its default connection settings as overridable parameters, which means the defaults are documented rather than folklore:

SettingKeyDefaultWhat blowing it looks like
Connect timeoutct5,000 msTwilio gave up before your server accepted the TCP connection
Read timeoutrt15,000 msConnection opened, response never finished
Total timett15,000 msHard ceiling on the whole request
Retry countrc1A single retry, not a queue that drains later
Retry policyrpctOnly connect timeouts are retried by default

That last row is the one people get wrong. With rp defaulting to ct, a 500 from your handler is not retried — Twilio tried once, your code threw, and the call is over. If your handler does anything slow (a database write, an LLM call, a third-party lookup), return TwiML immediately and do the work asynchronously. Twilio's own 11200 guidance says to "respond immediately with 200 OK or 204 No Content and process asynchronously if needed."

Parse the body as a form, not as JSON. Twilio sends application/x-www-form-urlencoded, and a handler wired for JSON reads an empty body, then throws on the missing From or CallSid. In Express that means express.urlencoded(); in Flask, request.form rather than request.json. A Twilio developer evangelist gives exactly this answer on the Stack Overflow thread where a webhook returns 400 on every request.

Malformed XML gets you 12100. Twilio's documented causes are narrow and mechanical: a leading space or blank line before the XML declaration, a missing <Response> root, unclosed elements, unquoted attributes, wrong capitalization (Twilio verbs start with capitals), and unescaped <, >, or & inside text. A template engine that emits a newline before the declaration will break TwiML that looks perfect in your editor.

To test the whole response at once, extend the basic curl check from our error 31000 guide with timing and header output, so a slow response fails the test as loudly as a broken one:

curl -o /dev/null -s \
  -w 'status=%{http_code}  type=%{content_type}  total=%{time_total}s\n' \
  -X POST https://your-server.com/voice \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'CallSid=CAtest0000000000000000000000000000' \
  -d 'From=%2B15551234567' \
  -d 'To=%2B15559876543'

You want status=200, a type of text/xml or application/xml, and a total comfortably under 15 seconds. Run it against production, not localhost — the point is to test the path Twilio takes.

Webhook failures that happen mid-call rather than at setup behave differently and are worth recognizing: a <Gather> action URL or status callback that fails after the call is live can drop the leg entirely, which surfaces as error 31005 gateway hangup or as a call that connects and then disconnects a second or two later.

Signature validation is rejecting real Twilio requests

Here is the failure that wastes the most time, because everything is configured correctly and the requests are genuinely from Twilio — your own middleware is throwing them away. It typically appears the day you deploy to production, since local development runs with validation switched off.

Confirm the diagnosis in one move: disable signature validation temporarily and retry. If the webhook fires, the URL your code reconstructs does not match the URL Twilio signed. Turn validation back on immediately afterward and fix the reconstruction.

Twilio computes the signature by taking the full URL it requested, appending POST parameters sorted alphabetically with no delimiters, then HMAC-SHA1ing that string with your Auth Token. Any difference in the URL string produces a different hash, and these are the differences that bite:

  • Scheme, changed by a proxy. Twilio requested https://, your TLS-terminating load balancer forwards plain HTTP, and your framework reconstructs http:// from the inbound request. Configure the framework to trust the proxy and read X-Forwarded-Proto — in Express that is app.set('trust proxy', true).
  • The port. Twilio's rules differ by protocol: for voice callbacks over HTTPS it drops the port before computing the signature, while over HTTP it keeps it. A framework that helpfully reconstructs https://example.com:443/voice fails validation for that reason alone.
  • Trailing slashes. Twilio's security documentation flags this directly: https://example.com/twilio/ and https://example.com/twilio are different strings, and servers that rewrite one into the other break the signature. Configure the URL in Twilio to match exactly what your router serves.
  • Re-encoded query strings. "Always use the exact URL Twilio used to make the request, including any URL-encoded characters. If you decode or re-encode the URL, signature validation fails." A %20 normalized to + invalidates the whole hash.
  • Dropped blank parameters. Twilio sends empty-valued fields, and several body parsers discard them. One developer chased this through Twilio support on an AWS Lambda handler before finding that Python's parse_qs needs keep_blank_values=True — without it, one missing empty string changes the signed payload and every request looks forged.

If you serve JSON-bodied webhooks, the mechanism changes: Twilio appends a bodySHA256 query parameter holding a SHA-256 of the raw body, and you must validate against the raw string rather than a reserialized object.

Debugging webhooks locally with the ngrok inspector

When you are developing against a tunnel, ngrok's web inspection interface at http://localhost:4040 answers the "did it even arrive" question faster than the Twilio Console does, because it sits on your side of the connection.

Every request through the tunnel appears there with full headers, parameters, and response body, and ngrok pretty-prints and validates XML — which means a TwiML document that would trigger 12100 shows its syntax error right in the inspector. You can filter by path, status code, or duration, and the Replay button re-sends any captured request without placing another real call. Fix your handler, hit Replay, read the new response. That loop is several minutes shorter than dialing the number again each time.

The reading is binary and useful either way. If a request appears in the inspector, Twilio reached you and the bug is in your handler. If nothing appears while the Twilio Debugger shows an 11200, the request died between Twilio and your tunnel — a rotated URL, a stale Twilio config, or a corporate firewall.

One trap specific to tunnels: signature validation must run against the public tunnel URL, not the local one your server sees. Your app receives a request for http://localhost:3000/voice while Twilio signed https://abc123.ngrok.app/voice, so unless you pass the public URL explicitly, validation fails for every local request even though the config is right.

FAQ

Can I allowlist Twilio's IP addresses for my webhook? No. Twilio's webhook FAQ states that "Twilio uses a pool of IP addresses to send webhook requests. As such, there is not a specific range of IP addresses that you should allow for Twilio webhooks." Use X-Twilio-Signature validation for authenticity, and put your webhook endpoints behind a DMZ or proxy rather than an IP filter.

Does a Twilio webhook have to use HTTPS? Not strictly, but use it. Twilio supports plain HTTP and adjusts its signature rules accordingly, keeping the port over HTTP and dropping it over HTTPS. The hard constraint is certificate quality: "Twilio webhooks do not work on HTTPS endpoints with self-signed SSL certificates," so staging boxes need a real certificate.

My number is in a Messaging Service — why doesn't its webhook fire? Check the service's Integration setting. Messaging Services default to "Defer to sender's webhook," which honors the number's own URL, so an override means someone changed it — most often by adding the number to the Default Conversations Service, which autocreates conversations instead. Set Integration back to defer, or remove the number from the sender pool.

What does the fallback URL do when a webhook fails? Twilio calls the VoiceFallbackUrl on the number or TwiML App when it can't retrieve or execute the primary TwiML. Twilio recommends hosting it "on separate infrastructure" so a failure of your main app doesn't take the fallback with it. A fallback pointing at a dead host adds a second error rather than a recovery.

Is Twilio down, or is my webhook broken? Check status.twilio.com to rule it out in seconds, then assume it's yours. Webhook failures are overwhelmingly application-side, and the Debugger proves it: an 11200 entry showing your server's 500 or a 15-second timeout is a record of your infrastructure, not Twilio's.

The webhook you don't have to run

With Alloqui there is no voice webhook to build or keep alive. You install @alloqui/dialer, paste your own Twilio keys, and drop a <Dialer /> into your React app. We host the voice webhook, validate every X-Twilio-Signature per tenant, and point an auto-provisioned TwiML App at it — so your server needs zero telephony endpoints, no public URL, and no tunnel that expires on a Friday afternoon. Nothing in this guide becomes your problem. Try Alloqui.