Stripe dice No signatures found matching the expected signature for payload. GitHub dice We could not verify your signature. Shopify dice HMAC validation failed. Tres errores distintos, pero la causa raiz suele ser una de siete cosas, y el 90% de las veces es la primera.
Este articulo ordena las siete causas por frecuencia, con un fix copiable y un comando de debug local de una linea para cada una.
Resumen en 30 segundos
- La firma del webhook = HMAC-SHA256(secret, raw_body). La formula nunca cambia; lo que cambia es el formato del header y los detalles de codificacion.
- Las 7 causas por frecuencia: body parseado en vez de raw / secret incorrecto / encoding incorrecto (hex vs base64) / tolerancia de timestamp / secret commiteado en codigo / usar === para comparar / secret filtrado en logs
- Cada provider usa formato y encoding distintos: GitHub usa sha256= mas hex, Shopify usa base64 crudo, Slack usa v0= mas hex, Stripe usa t=…,v1= mas hex
- Usa el Validador de Firma de Webhook de Piick en tu navegador para reproducir cualquier firma localmente y ver si coincide, los 5 providers soportados
Las 7 causas, de la mas comun a la mas sutil
Causa 1: Verificas el body parseado como JSON, no el raw body
Esta es la causa raiz del 50% de los fallos de firma. Todos los providers firman raw bytes, pero tu estas firmando JSON.stringify(req.body). Express, Flask y Next.js parsean el body por defecto y luego lo re-stringifican, el orden de keys o los espacios cambian, y la firma ya no coincide.
El fix:
- Express: usa express.raw() en vez de express.json(), en el handler pasa req.body (Buffer) directamente al verificador
- Flask: usa request.get_data(as_text=False), no request.json
- Next.js API route: agrega export const config con api bodyParser false
Debug: imprime los primeros 30 caracteres de req.body y Content-Length, luego compara con los datos POST originales.
Causa 2: Secret incorrecto
En local usas el secret efimero de Stripe CLI (impreso por stripe listen). En produccion usas el del dashboard. El mismo endpoint tiene distintos secret strings en dos entornos.
El fix: configura cada endpoint en cada entorno de forma independiente. Lee env vars desde un secret manager. Nunca hardcodees en el codigo.
Debug: verifica el mismo valor de secret en tres lugares — dashboard, Stripe CLI, env del codigo — y confirma que coinciden exactamente.
Causa 3: Encoding incorrecto
El 90% de los providers usan hex, pero Shopify solo usa base64. Si envias hex a Shopify, la firma nunca coincidira.
El fix: sigue el spec del provider estrictamente, no alternes hex / base64 manualmente. Usa el dropdown de provider del validador de webhook de Piick.
Debug: compara la longitud del hex generado (64 caracteres) contra la longitud del base64 (44 caracteres). Longitud correcta usualmente significa formato correcto.
Causa 4: Tolerancia de timestamp no configurada o mal configurada (solo Stripe)
Stripe envio el webhook hace 5 minutos, pero tu servidor lo rechaza. Stripe incluye un timestamp en t=…, y el SDK solo acepta requests dentro de ±5 minutos por defecto.
El fix: Stripe.webhooks.constructEvent(payload, sig, secret, tolerance=300). 300 segundos es suficiente para produccion, pero no lo pongas muy grande (es tu ventana de proteccion contra replay).
Debug: imprime la diferencia now - t. Si es mayor que 300, tu reloj de servidor esta derivando. Sincroniza NTP.
Causa 5: Secret commiteado en codigo
El secret se filtra, y un atacante forja webhooks con el secret real.
El fix: rota el secret inmediatamente (env var desde secret manager). Usa git log -p mas grep -i whsec para encontrar fugas historicas.
Debug: trata el secret como una contrasena — nunca lo dejes entrar al control de versiones.
Causa 6: Comparando firmas con ===
Funcionalmente funciona, pero teoricamente vulnerable a ataques de timing remotos. Los atacantes envian requests repetidos y miden las diferencias de tiempo de respuesta para crackear la firma byte a byte. El crypto.timingSafeEqual de Node y el hmac.compare_digest de Python son comparaciones constant-time, inmunes a esto.
El fix:
- Node: crypto.timingSafeEqual(Buffer.from(sig1), Buffer.from(sig2))
- Python: hmac.compare_digest(sig1, sig2)
Debug: grep el codigo por === y ==. Donde compares firmas, cambia a la version timing-safe.
Causa 7: Secret filtrado en logs durante debug
Funciona en local, falla para todos los webhooks en produccion. Causa: logueaste el secret durante el debug (o lo commiteaste), luego lo rotaste. Los webhooks ahora firman con el nuevo secret, pero la env var de produccion no se actualizo.
El fix: loguea solo el estado de coincidencia, no el secret ni la firma. console.log(sig verify, matched: true).
Debug: git log -p grep por palabras clave del secret. Confirma que ningun commit historico lo filtro.
Como reproducir cualquier firma localmente
- Usa la pestana Compute del Validador de Firma de Webhook de Piick. Llena secret, raw body y provider, el header de firma generado va directo a tu config de webhook.
- Usa la pestana Verify para pegar el header recibido. Ve si coincide, retroalimentacion instantanea, sin console.log ni reinicio de servicio.
- Esta herramienta es totalmente local (Web Crypto API del navegador), no sube payload / secret, segura para usar secrets reales durante debug.
Cheat sheet de formato de firma de 5 providers
| Provider | Header | Encoding | Payload firmado |
|---|---|---|---|
| Stripe | Stripe-Signature: t=[ts],v1=[hex] | hex | [ts].[raw body] |
| GitHub | X-Hub-Signature-256: sha256=[hex] | hex | raw body |
| Shopify | X-Shopify-Hmac-SHA256: [base64] | base64 | raw body |
| Slack | X-Slack-Signature: v0=[hex] | hex | v0:[ts]:[raw body] |
| Generic | personalizado | hex | raw body |
- Shopify es el unico que usa base64, los demas usan hex
- Solo Stripe y Slack incluyen timestamp
- Solo GitHub (sha256=) y Slack (v0=) tienen prefijo
- Ve la referencia completa eligiendo un provider en webhook-signature-validator. Cada uno trae un payload de ejemplo integrado.
Practicas recomendadas
- Usa express.raw() / request.get_data() para manejar webhooks, nunca JSON.stringify(parsedBody)
- Sigue el encoding del provider estrictamente. Stripe / GitHub / Slack = hex, Shopify = base64
- Compara firmas con timingSafeEqual / compare_digest, no con ===
- Configura tolerancia de timestamp de 5 minutos para Stripe para defenderte de replay attacks
- Los secrets siempre van por env vars, nunca en codigo, nunca en logs
Quieres verificar una firma tu mismo? Usa el Validador de Firma de Webhook de Piick en tu navegador para computar y verificar firmas, los 5 providers cubiertos, los datos quedan en el navegador. Combinalo con el Decodificador JWT para cobertura completa de tu stack de herramientas de auth y seguridad de webhooks.