Validador de firma de webhook
Tu proveedor envía una cabecera de firma, tu handler dice que no coincide, y la documentación responde con un SDK para instalar. Eso no te dice por qué. Pega la firma, tu secreto y el cuerpo en bruto abajo: recalculamos el HMAC en tu navegador y, si falla, probamos las mutaciones que suelen causarlo — un espacio de más en el secreto, un salto de línea final, un cuerpo re-serializado, un timestamp caducado.
En resumen: una firma de webhook falla casi siempre por una de cinco razones — secreto incorrecto (test vs live), un cuerpo re-serializado por tu framework, un timestamp fuera de la ventana de 5 minutos, un salto de línea final o un prefijo sha256= / v0= que quedó. Esta herramienta recalcula el HMAC y te dice cuál.
Pega una firma que falla
Los cuatro esquemas de firma
Los cuatro son HMAC, pero difieren en qué se hashea y cómo se codifica el resultado. Un detalle mal y falla cada entrega.
| Proveedor | Qué se firma | Algoritmo |
|---|---|---|
| Stripe | HMAC de t.body; firma y timestamp en una cabecera Stripe-Signature | SHA-256 hex |
| GitHub | HMAC del cuerpo en bruto; firma en X-Hub-Signature-256 | SHA-256 hex |
| Shopify | HMAC del cuerpo en bruto; firma en X-Shopify-Hmac-Sha256 | SHA-256 base64 |
| Slack | HMAC de v0:timestamp:body; también necesita la cabecera timestamp | SHA-256 hex |
Twilio es la excepción — hashea una URL canónica más parámetros ordenados, no el cuerpo. Usa el validador de firma de Twilio para eso.
Las cinco causas reales
Ordenadas por cuánto aparecen en los hilos de soporte.
| # | Causa | Síntoma | Solución |
|---|---|---|---|
| 1 | Secreto incorrecto | Toda entrega falla, incluso una nueva | Confusión test vs live — cada proveedor emite un secreto de firma distinto por modo y por endpoint. |
| 2 | Cuerpo re-serializado | Falla tras parsear el JSON tu framework | Firma los bytes en bruto, no JSON.stringify(parsed): el espaciado y el orden de claves cambian el hash. |
| 3 | Timestamp caducado | Pasa en pruebas, falla con eventos reenviados | Stripe y Slack rechazan lo que supere 5 minutos. No reenvíes un evento capturado horas después. |
| 4 | Salto de línea final | Cuerpo desviado un byte, firma nunca válida | Un proxy o editor añadió un salto de línea. La herramienta detecta ese caso exacto. |
| 5 | Prefijo comparado literal | sha256= o v0= incluido en la comparación | Compara solo la parte hex/base64, no el prefijo del esquema. |
La trampa del cuerpo en bruto
La causa número uno: verificas contra el cuerpo parseado-y-re-serializado en vez de los bytes exactos. JSON.stringify(JSON.parse(body)) cambia el espaciado y el orden de claves, así que el HMAC ya no coincide. Lee el cuerpo en bruto antes de que algo lo parsee.
// Express: keep the RAW bytes, do NOT let JSON re-serialise them
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const raw = req.body; // Buffer, exactly as received
verify(raw, req.headers['stripe-signature'], secret);
});# Flask: request.get_data() returns the raw body; request.json does NOT raw = request.get_data() # bytes, untouched verify(raw, request.headers['X-Hub-Signature-256'], secret)
// Next.js App Router: read the text before parsing
export async function POST(req) {
const raw = await req.text(); // sign THIS, not JSON.stringify(await req.json())
verify(raw, req.headers.get('stripe-signature'), secret);
}Regla práctica: captura la firma y los bytes en bruto en el borde, verifica, luego parsea.
Timestamps y reenvíos
Stripe y Slack integran un timestamp en la cadena firmada y rechazan lo que supere cinco minutos — es protección anti-reenvío, no un bug. GitHub y Shopify no, así que una firma de GitHub válida lo sigue siendo para siempre. Si la herramienta dice válida pero caducada, tu secreto y tu cuerpo están bien; solo reenvías un evento viejo. Genera una entrega nueva en vez de reenviar una capturada.
Cuando la firma es correcta
¿Necesitas enviar un evento de prueba bien firmado a tu endpoint? El firmador de webhooks construye uno para los seis proveedores. Para capturar e inspeccionar entregas reales, apúntalas a un túnel Relay.
Preguntas frecuentes
¿Guardáis mi secreto de firma?
No. El HMAC se calcula en tu navegador con Web Crypto; el secreto y el cuerpo nunca llegan a nuestros servidores. Abre la pestaña Red: no sale ninguna petición al pulsar Verificar.
¿Qué proveedores se admiten?
Stripe, GitHub, Shopify y Slack — los cuatro esquemas HMAC sobre el cuerpo. Twilio firma una URL canónica en su lugar, así que tiene su propia herramienta: el validador de firma de Twilio.
Pasa aquí pero mi servidor lo rechaza igual. ¿Por qué?
Casi siempre el cuerpo. Tu framework parseó el JSON y lo re-serializaste antes de verificar, lo que cambia el espaciado y el orden de claves. Firma los bytes en bruto — mira los fragmentos de Express, Flask y Next arriba.
¿Qué significa «válida pero caducada»?
El HMAC es correcto, pero Stripe y Slack también rechazan firmas de más de cinco minutos para bloquear reenvíos. Si capturaste un evento y lo reenvías después, la verificación falla por el timestamp aunque el secreto sea correcto.