Objective
This article explains how to resolve Twilio request validation failures that cause your webhook to return HTTP 403 Forbidden and trigger Error 11200 (HTTP retrieval failure) when your application runs behind a proxy or reverse-proxy layer such as Google Cloud Functions, AWS Lambda with API Gateway, ngrok, Heroku, or any environment using HTTPS termination upstream. The root cause is almost always a mismatch between the URL Twilio used to sign the request and the URL your application reconstructs when calling the RequestValidator. This guide shows how to reconstruct the URL correctly so that validation succeeds.
Product
- Programmable Messaging
- Programmable Voice
User Account Permission/Role(s) Required
Account Owner, or admin, and access to their Application Code.
Procedure
Step 1: Confirm the symptom
You will see one or more of the following symptoms:
Error 11200 in the Twilio Console (Messaging or Voice logs) with the message HTTP retrieval failure.
- Your webhook logs show HTTP 403 responses to incoming Twilio requests.
- Your application log prints Valid Twilio Request: False (or the equivalent in your SDK).
The same code works locally when tested directly (for example, against localhost) but fails once deployed behind a proxy, load balancer, or tunnel.
Step 2: Understand the root cause
When Twilio sends a webhook, it computes the X-Twilio-Signature header by hashing the full URL it called (scheme, host, path, and any query parameters) together with the POST body and your Auth Token. Your application must reconstruct the exact same URL to validate the signature.
When your application sits behind a proxy that terminates HTTPS, common framework objects such as request.url or request.host may report the internal scheme (often http) or a partial URL without the path. If the reconstructed URL does not exactly match what Twilio used, the signature will not match and validation will fail.
The two most common reconstruction mistakes are:
- Wrong scheme: Building the URL with http:// when Twilio called https://, because the proxy forwarded the request internally over HTTP.
- Missing path: Building the URL as https://host without including the path (for example /my-webhook), so it does not match the full URL Twilio called.
Step 3: Verify what URL Twilio called
Before changing your code, confirm the exact webhook URL configured in the Twilio Console:
- Open the Twilio Console and navigate to the phone number, Messaging Service, or TwiML App that handles your inbound traffic.
- Copy the full webhook URL exactly as it appears (including scheme, host, path, and any query parameters).
- Compare it to the URL your application is passing to the validator. If they differ in scheme, host, or path, that is your problem.
Step 4: Reconstruct the URL correctly (Python / Flask)
The recommended pattern is to start from the framework's full URL (which already includes the path) and only override the scheme using the proxy's forwarded-protocol header:
from functools import wraps
from flask import request, abort
from twilio.request_validator import RequestValidator
import os
def validate_twilio_request(f):
@wraps(f)
def decorated_function(*args, **kwargs):
validator = RequestValidator(
os.environ['TWILIO_AUTH_TOKEN']
)
# Start from the framework's full URL (includes path and query)
# and override the scheme using X-Forwarded-Proto when behind a proxy.
forwarded_proto = request.headers.get(
'X-Forwarded-Proto',
'https'
)
url = request.url.replace(
'http://',
forwarded_proto + '://',
1
)
request_valid = validator.validate(
url,
request.form,
request.headers.get('X-Twilio-Signature', '')
)
if request_valid:
return f(*args, **kwargs)
return abort(403)
return decorated_function
Equivalent fix in Node.js / Express:
const express = require('express');
const { validateRequest } = require('twilio');
const app = express();
app.set('trust proxy', true);
app.use(
express.urlencoded({ extended: false })
);
app.post('/sms', (req, res) => {
const signature =
req.header('X-Twilio-Signature') || '';
const url =
`${req.protocol}://${req.get('host')}${req.originalUrl}`;
const valid = validateRequest(
process.env.TWILIO_AUTH_TOKEN,
signature,
url,
req.body
);
if (!valid) {
return res.status(403).send('Forbidden');
}
// ... handle the request
});
Step 5: Test the fix
- Deploy the updated code to the same environment where the failure occurred (do not test only locally, since the proxy behavior is the variable you are trying to validate).
- Trigger an inbound message or call to your Twilio number so a webhook is sent.
- Confirm in your application logs that the reconstructed URL matches the URL configured in the Twilio Console exactly.
- Confirm the validator now returns True, your webhook returns a 2xx response, and Error 11200 no longer appears in the Messaging or Voice logs.
Step 6: Additional troubleshooting
If validation still fails after applying the fix above, check the following:
- Auth Token. Confirm the Auth Token used by RequestValidator belongs to the same Twilio account (or subaccount) that owns the phone number or Messaging Service receiving the traffic. If the Auth Token was recently rotated, update the environment variable accordingly.
- Query parameters. If the webhook URL contains query parameters in the Twilio Console, your reconstructed URL must include them in the same order. Building the URL from request.url or req.originalUrl preserves them automatically.
- Trailing slashes. A trailing slash difference (for example /webhook vs /webhook/) will break the signature. The reconstructed URL must match the Console exactly.
- POST body. The validator hashes the form-encoded POST parameters. If middleware modifies, re-encodes, or parses the body before validation, the hash will not match. Validate before any body transformation.
Additional Information
Secure your Flask App by Validating Incoming Twilio Requests
Secure your Express app by validating incoming Twilio requests
11200 – HTTP retrieval failure
- Webhooks Security