Oferta de lanzamiento: −50 % en tu primer pago, hasta el 30 de noviembre.Ver la oferta →
webhook·toolkit
Twilio

Falla la validación de la firma de Twilio — descubre por qué

Última actualización: julio de 2026

Tienes una cabecera X-Twilio-Signature, tu código dice que no coincide y la documentación de Twilio responde con un SDK que instalar. Eso no te dice qué falla: tu URL, tus parámetros o tu token. Así que pega los tres aquí abajo: recalculamos la firma para cada forma plausible de la URL y te mostramos la que coincide. La línea que coincide es tu diagnóstico.

En resumen: una firma de Twilio falla casi siempre porque la URL que reconstruye tu código no es idéntica byte a byte a la URL a la que llamó Twilio — normalmente http:// en lugar de https:// detrás de un proxy inverso. El algoritmo rara vez es el problema.

Comprueba una firma que Twilio rechazó

Todo se ejecuta en tu navegador (HMAC-SHA1 con WebCrypto). Tu Auth Token nunca se envía a nuestros servidores y ninguna petición sale de esta página mientras usas la herramienta. Abre la pestaña de red y compruébalo — preferimos que lo hagas.

La cadena exacta que firma Twilio

Todo lo demás en esta página se deriva de esto. Twilio no firma tu cuerpo ni tus cabeceras. Firma una sola cadena, construida así:

  1. Empieza por la URL completa a la que llamó Twilio: esquema, host, puerto si era explícito, ruta y query string.
  2. Ordena los nombres de los parámetros POST en orden ASCII.
  3. Para cada uno, añade el nombre seguido inmediatamente de su valor. Sin separador, sin signo igual, sin codificación, nada en medio.
  4. Calcula el HMAC-SHA1 de toda la cadena con tu Auth Token.
  5. Codifica el digest en Base64. Eso es X-Twilio-Signature.

Lo mismo con valores reales

URL a la que llama Twilio
https://api.example.com/twilio/sms
Parámetros POST (form-urlencoded)
AccountSid=AC1234567890abcdef
Body=Hello
From=%2B15017122661
To=%2B15558675310
La cadena firmada — 101 caracteres, concatenados
https://api.example.com/twilio/smsAccountSidAC1234567890abcdefBodyHelloFrom+15017122661To+15558675310
Auth Token (desechable, para el ejemplo)
12345678901234567890123456789012
base64(HMAC-SHA1(string, token))
REsmqJpWLoqe1Q/VJXTZjx4eviY=

Fíjate en lo que no aparece. Ni &, ni =, ni codificación por porcentaje: el valor es +15017122661, no %2B15017122661. Quien reconstruye los parámetros troceando el cuerpo en bruto por su cuenta suele olvidarse de decodificar, y obtiene una firma errónea por un motivo que ningún mensaje de error va a mencionar.

Cambia un solo byte de esos 101 caracteres y la firma cambia por completo. Esa es toda la historia: tu código y Twilio tienen que construir exactamente la misma cadena, y cada proxy, router y middleware intermedio puede cambiarla sin avisarte.

Cuatro URL, cuatro firmas, una sola verdad

Mismo Auth Token, los mismos cuatro parámetros de arriba. Solo cambia la URL — y basta con eso:

URL usada para construir la cadenaFirma resultanteDe dónde viene
https://api.example.com/twilio/smsREsmqJpWLoqe1Q/VJXTZjx4eviY=Lo que firma Twilio
http://api.example.com/twilio/smsNpKfrAiY/l6i2mXxfWlrjyRkytY=Lo que ve tu app detrás de la terminación SSL
https://api.example.com:443/twilio/smsvMbUG7KsMVUK80lzK6a+4IMFPqU=Así queda una URL construida a mano
https://api.example.com/twilio/sms/8LRHF+mPXykd0Jij9WhgsI7agMs=Lo que te da un router que normaliza rutas

Nada en el fallo te dice en cuál de las cuatro estás. Eso es lo que hace la herramienta: las calcula todas, hasta dieciséis formas de URL, y compara cada una con la firma que Twilio envió de verdad. La línea verde es la respuesta.

Las 8 causas reales

Ordenadas según la frecuencia con que aparecen en el foro de la comunidad de Twilio y en los hilos de StackOverflow sobre este error. Ese orden es nuestra lectura, no una estadística medida: tómalo como un orden de búsqueda, no como un hecho.

#CausaEl síntoma que la delataSolución
1Terminación SSL (X-Forwarded-Proto)Pasa con curl en localhost y falla en cuanto está detrás de un balanceador de carga. Tu log muestra http://, la consola muestra https://.Confía en el proxy: app.set('trust proxy', true), ProxyFix(app.wsgi_app, x_proto=1), TrustedProxies en Laravel.
2Puerto explícito en la URL reconstruidaTu log muestra https://host:443/… — Twilio nunca escribe un puerto implícito.Constrúyela a partir de la cabecera Host / X-Forwarded-Host. No concatenes nombre de host y puerto.
3Query string eliminada o recodificadaSolo fallan las URL que llevan ?params. Tu framework devolvió el patrón de la ruta, no el destino en bruto.Usa el destino en bruto de la petición (req.originalUrl, request.url) y no toques la codificación.
4Parámetros alterados antes de validarSolo falla en algunos mensajes: los que tienen un campo vacío o espacios al final.Valida antes de que ningún middleware toque el cuerpo. Python: parse_qs(…, keep_blank_values=True). Laravel: excluye la ruta de TrimStrings.
5Cuerpo JSON validado como formularioFallan los webhooks de Conversations y Studio; los de SMS normales pasan.Usa validateRequestWithBody() y el parámetro de query bodySHA256.
6Auth Token secundario sin promoverEmpezó a fallar justo después de rotar el token, en el 100 % de las peticiones.Console → Account → Auth Token: un token secundario no firma nada hasta que lo promueves a Primary.
7Parámetros repetidos (bug del SDK)Solo fallan la mensajería de grupo y Conversations, y no de forma reproducible.twilio-node #722: params['MessagingBinding.Address'].sort() antes de validateRequest().
8Reescritura del proxy que cambia la rutaSolo falla en producción. Twilio llama a /webhooks/sms y tu app recibe /sms.Valida contra la ruta pública, no contra la interna que queda tras la reescritura.

El bug de los parámetros repetidos — no es culpa tuya

Este merece su propia sección, porque puedes leer la documentación de Twilio durante una hora sin encontrarlo. La mensajería de grupo y Conversations envían la misma clave varias veces:

El cuerpo que Twilio envía por POST
MessagingBinding.Address=%2B15558675310&MessagingBinding.Address=%2B15017122661

El servidor de Twilio ordena las claves y luego concatena. Tu mapa de parámetros contiene un array. twilio-node hace data += key + params[key], y JavaScript convierte ese array en cadena uniéndolo con comas — en orden de llegada. Si tu framework parseó las dos direcciones en un orden distinto del que usó el servidor de Twilio, las dos cadenas difieren y la validación falla. Mismo token, misma URL, mismos parámetros. Y falla igualmente.

Dos órdenes, dos firmas — todo lo demás idéntico
Orden de llegada: +15558675310 y luego +15017122661LJaNX3wj9/RRc4+GG1kwgPndM1c=
Ordenado: +15017122661 y luego +15558675310A7jNj66u8x1pDaqy3CNma9Q0XvA=
La solución provisional
// twilio-node issue #722
// Los parámetros repetidos te llegan como array; ordénalos antes de validar.
if (Array.isArray(params['MessagingBinding.Address'])) {
  params['MessagingBinding.Address'].sort();
}
const valid = twilio.validateRequest(authToken, signature, url, params);

La herramienta prueba los dos órdenes automáticamente. Si la línea ordenada sale en verde, has encontrado un bug en el SDK, no en tu código — y puedes dejar de releer tu handler.

Funciona en local, falla en producción

La historia más frecuente, y tiene una sola causa: en desarrollo, tu túnel entrega la petición a tu app con el mismo esquema que usó Twilio. En producción, Traefik, nginx o Cloudflare terminan el TLS y reenvían HTTP plano al puerto 3000. Tu framework informa de lo que ve en su socket — http:// — y firma una cadena que Twilio nunca construyó.

Express
// Detrás de Traefik / nginx / Cloudflare
app.set('trust proxy', true);
// req.protocol ahora sigue a X-Forwarded-Proto
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
Flask / Werkzeug
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
# request.url ahora reconstruye https://
validator.validate(request.url, request.form, signature)
Laravel
// app/Http/Middleware/TrustProxies.php
protected $proxies = '*';           // solo si el proxy es tuyo
protected $headers = Request::HEADER_X_FORWARDED_ALL;

Una línea, tres ecosistemas. Una vez que confías en el proxy, el framework lee X-Forwarded-Proto y X-Forwarded-Host, reconstruye https:// y la firma coincide. Confía solo en proxies que controles de verdad: con trust proxy en true, cualquier cosa que haya delante puede declarar el esquema que quiera.

Cuerpos JSON: bodySHA256

Los webhooks de Twilio son application/x-www-form-urlencoded por defecto — nunca JSON. Pero Conversations y Studio pueden enviar JSON, y entonces cambian las reglas: el cuerpo no está en los parámetros. Twilio lo hashea con SHA-256, añade ese hash a la URL como parámetro de query bodySHA256 y firma la URL con una lista de parámetros vacía.

Validar un webhook JSON (Node)
const valid = twilio.validateRequestWithBody(
  process.env.TWILIO_AUTH_TOKEN,
  req.headers['x-twilio-signature'],
  fullUrl,        // debe seguir llevando ?bodySHA256=…
  rawBody         // la cadena en bruto, NO pasada por JSON.parse y re-stringify
);

Aquí hay dos trampas. Si quitas la query string de la URL, también quitas bodySHA256, y la comprobación falla sin mencionar nunca el cuerpo. Y si tu framework parseó el JSON antes de que llegaras, JSON.stringify() no te devolverá los bytes originales: el orden de las claves y los espacios se han perdido. Captura el cuerpo en bruto. El modo JSON de la herramienta de arriba muestra los dos hashes lado a lado para que veas cuál de las dos trampas te ha mordido.

Datos de Twilio que conviene tener presentes

Detalles que deciden si la firma llega siquiera a comprobarse:

  • HMAC-SHA1, no SHA256. Stripe, GitHub y Shopify usan SHA-256, así que copiar un handler que funciona de uno de ellos y cambiar el nombre de la cabecera da un validador que nunca coincide con nada.
  • El payload es application/x-www-form-urlencoded por defecto, nunca JSON. Si tu framework solo parsea cuerpos JSON, tu mapa de parámetros está vacío y todas las firmas fallan.
  • Twilio espera 15 segundos por un webhook de voz y 5 segundos en Conversations. Valida primero, responde y después haz el trabajo lento: una comprobación de firma que se ejecuta después de una llamada a la base de datos puede agotar el tiempo antes de devolver.
  • Un reintento por defecto, y hasta cinco con connection overrides. Así que una firma incorrecta no es un mensaje fallido: es un mensaje perdido tras un par de intentos.
  • El Debugger conserva sus logs 30 días. Ahí encontrarás la URL a la que llamó Twilio de verdad, que es el dato que necesita esta página.
  • En las conexiones WebSocket la cabecera llega en minúsculas: x-twilio-signature. Compara las cabeceras sin distinguir mayúsculas y minúsculas.

¿Y el error 57012 de Twilio?

El error 57012 (firma no válida) te dice que la firma no coincide con la que calculó Twilio y señala a los sospechosos habituales: token incorrecto, URL incorrecta, parámetros modificados por el camino, hash del cuerpo ausente. Todo cierto. Pero se detiene justo donde empieza la depuración, porque no puede decirte cuál se aplica a tu petición. Ten en cuenta además que, desde el lado de Twilio, las causas 1, 2, 3, 7 y 8 de arriba son todas «URL incorrecta»: cinco bugs distintos, cinco soluciones distintas, un solo código de error. Separarlos es el objetivo de esta página.

Cuándo usar qué — con honestidad

Aquí no somos la respuesta a todo, y fingir lo contrario te haría perder la tarde.

La documentación de Twilio

Estás escribiendo el handler desde cero y quieres el algoritmo de referencia y el SDK para tu lenguaje. Es la fuente de verdad — webhooks-security cubre siete lenguajes. No tiene ninguna sección de resolución de problemas, y por eso has acabado aquí.

Hookdeck

Tu problema no es una petición rota, es la entrega: quieres reintentos que controlas tú, una dead letter queue, timeouts más allá de los 15 segundos de Twilio, transformación de formulario a JSON o verificación de firmas en el edge para que tu app nunca vea una petición sin verificar. Eso es una plataforma, y de las de verdad. Págala.

Esta página

Una firma, ahora mismo, y quieres saber qué línea de tu código miente. Treinta segundos, nada que instalar, sin cuenta. Luego vas a arreglar la configuración del proxy y te olvidas de que existimos. Es un buen final.

Si los webhooks son una parte permanente de tu stack y no un incendio que estás apagando, nuestro túnel Relay reenvía el tráfico real de Twilio a localhost, y el tester de webhooks de Twilio reenvía peticiones firmadas a tu handler para que compruebes el arreglo sin gastar un SMS real.

Probar webhook TwilioRelay

Preguntas frecuentes

¿Por qué falla mi firma de Twilio detrás de ngrok o de un proxy?

Porque el proxy termina el TLS y reenvía HTTP plano a tu app. Twilio firmó la URL https://; tu framework reconstruye http:// desde su propio socket. Las cadenas difieren y el HMAC también. Confía en el proxy para que se use X-Forwarded-Proto al reconstruir la URL, y vuelve a coincidir.

¿Hay que incluir el puerto en la URL?

Solo si Twilio lo usó. Twilio firma la URL tal como está configurada, así que un 443 estándar nunca aparece en la cadena. Si añades :443 al reconstruirla, obtienes otra firma. Reconstruye el host a partir de la cabecera Host en lugar de concatenar nombre de host y puerto.

¿Cómo valido un cuerpo JSON de Twilio?

Con validateRequestWithBody() en lugar de validateRequest(). Twilio hashea el cuerpo con SHA-256, pone el hash en un parámetro de query bodySHA256 y firma la URL sin parámetros. Necesitas los bytes en bruto del cuerpo y la URL completa, incluido ese parámetro de query.

¿Por qué funciona en local pero no en producción?

En desarrollo, tu túnel suele dar a tu app el mismo esquema que usó Twilio. En producción, Traefik, nginx o Cloudflare le pasan HTTP plano en un puerto interno. Mismo código, URL reconstruida distinta, así que la firma deja de coincidir. Es la causa número uno de esta página.

¿Mi Auth Token se envía a vuestros servidores?

No. El HMAC se ejecuta en tu navegador con WebCrypto y la herramienta no envía ninguna petición mientras la usas. Abre la pestaña de red y compruébalo. Tu Auth Token es un secreto de producción: deberías preguntarle esto a cualquier sitio que te lo pida, incluido el nuestro.

¿La firma de Twilio es SHA-256, como la de Stripe?

No, Twilio usa HMAC-SHA1 codificado en base64. Stripe, GitHub y Shopify usan SHA-256. Adaptar un handler de Stripe que funciona cambiando el nombre de la cabecera te da un validador que lo rechaza todo, y es una forma habitual de llegar a este error.

Oferta de lanzamiento · −50 %

¿Depurando una integración de webhooks esta semana?

El Pass 7 días desbloquea las explicaciones con IA y el código del handler listo para pegar, 25 URL permanentes, 30 días de historial y el túnel Relay a localhost. Un solo pago, sin suscripción.

5 € 2,50 € una vez
Pago único · sin suscripción · reembolso en 14 días · precio de lanzamiento hasta el 30 de noviembre
Falla la validación de la firma de Twilio: encuentra la causa · Webhook Toolkit