Técnica · 7 de noviembre de 2026 · 6 min de lectura
Webhooks explicados de forma sencilla: firma, reintentos, seguridad
Qué es un webhook, en qué se diferencia de la consulta a una API y cómo tratar firma, reintentos y duplicados. Con curl y Node.
Un webhook es una llamada en sentido contrario. En lugar de que su sistema pregunte cada pocos minutos a un servicio si ha ocurrido algo, el servicio llama a una dirección suya en cuanto ocurre. Eso ahorra consultas y entrega las novedades más deprisa. A cambio, usted debe proporcionar un receptor y protegerlo. Este artículo explica ambas cosas.
Webhook o consulta a una API
| Consulta a una API (polling) | Webhook | |
|---|---|---|
| ¿Quién empieza? | Su sistema | El remitente |
| ¿Cuándo? | Según un calendario, por ejemplo cada hora | En cuanto ocurre un evento |
| Esfuerzo | Consultas aunque no haya novedades | Hace falta un receptor accesible |
| Bueno para | Informes, conciliación, recarga | Notificación, reenvío |
| Riesgo | Retraso hasta la siguiente consulta | El mensaje se pierde si el receptor no responde |
En la práctica, muchos combinan ambos: el webhook comunica el evento, la API obtiene los detalles si hace falta o cierra lagunas tras una caída.
Cómo transcurre una entrega
- Ocurre un evento, por ejemplo «pago recibido».
- El remitente envía un POST con JSON a la dirección que usted ha registrado (HTTPS).
- Su receptor comprueba la firma, memoriza el mensaje y responde rápido con un código del rango 2xx.
- El trabajo propiamente dicho lo realiza después su sistema.
El paso 3 es importante. Stripe recomienda en la documentación de webhooks devolver de inmediato un código 2xx, antes de que se ejecute una lógica costosa que podría provocar un timeout. Quien primero contabiliza la factura y después responde arriesga reintentos innecesarios.
Firma: cómo comprobar el origen
Su dirección es accesible públicamente. Cualquiera podría enviarle mensajes inventados. Por eso los remitentes serios firman cada mensaje con un secreto compartido. Es habitual un HMAC con SHA-256 sobre el contenido, a menudo junto con una marca de tiempo. GitHub describe el procedimiento así: HMAC-SHA256 sobre el contenido, representado en hexadecimal, con la cabecera X-Hub-Signature-256. La comparación debe hacerse con una función de tiempo de ejecución constante, para que los atacantes no deduzcan nada de los tiempos de respuesta.
Puntos importantes para su receptor:
- Usar el contenido en bruto. Calcule la firma sobre los bytes recibidos, no sobre el JSON serializado de nuevo. Hasta un salto de línea distinto cambia la firma.
- Comprobar la marca de tiempo. Stripe utiliza una marca de tiempo en la firma y una tolerancia por defecto de cinco minutos para evitar ataques de repetición. Una tolerancia de 0 desactiva la comprobación.
- Mantener secreto el secreto. No en el código, no en el repositorio, sino en una variable de entorno o en una caja fuerte.
La iniciativa Standard Webhooks reúne directrices de código abierto para remitentes y receptores. Cómo forma su firma cada proveedor en concreto figura en su documentación.
Reintentos y doble entrega
Si su receptor no responde o responde con un código de error, el remitente lo intenta de nuevo. Stripe, por ejemplo, según su propia documentación reintenta con intervalos crecientes hasta tres días. Otros proveedores tienen otras reglas, algunos desactivan el webhook tras demasiados fallos. De ello se sigue:
- Cuente con duplicados. El mismo evento puede llegar varias veces, aunque su receptor funcione correctamente. Stripe lo señala expresamente.
- Haga idempotente el tratamiento. Es decir: quien procesa dos veces el mismo mensaje no genera un segundo resultado. Guarde el identificador del evento y omita lo ya conocido.
- No confíe en el orden. Los eventos pueden llegar intercambiados. En caso de duda, compruebe el estado actual mediante la API.
Seguridad en ambos lados
Para receptores: solo HTTPS, comprobar firma y antigüedad, limitar el tamaño del mensaje, no ejecutar órdenes del contenido. Trate el contenido como cualquier entrada del exterior.
Para remitentes que llaman a direcciones de clientes: quien ofrece webhooks llama a direcciones que otros han registrado. Es una vía clásica para Server-Side Request Forgery. El atacante registra una dirección interna. El servidor la llama desde dentro de la red protegida. La descripción de OWASP menciona como objetivos, por ejemplo, servicios internos y direcciones de metadatos de proveedores de nube. Medidas de protección razonables:
- admitir solo
https://, - resolver la dirección al registrarla y antes de cada llamada y rechazar los rangos privados, locales y reservados,
- no seguir ciegamente las redirecciones,
- fijar límites de tiempo cortos.
Ejemplo: enviar un mensaje de prueba con curl
Con curl y openssl puede probar su receptor sin el remitente real. El script envía un mensaje firmado como lo enviaría un remitente con marca de tiempo:
GEHEIMNIS="test-geheimnis"
ZEIT=$(date +%s)
INHALT='{"id":"evt_1","typ":"besuch.begonnen","firma":"beispiel-firma.example"}'
SIGNATUR="sha256=$(printf '%s.%s' "$ZEIT" "$INHALT" | openssl dgst -sha256 -hmac "$GEHEIMNIS" | sed 's/^.* //')"
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "X-Zeit: $ZEIT" \
-H "X-Signatur: $SIGNATUR" \
-d "$INHALT"
Ejemplo: un pequeño receptor en Node.js
const http = require('http')
const crypto = require('crypto')
const GEHEIMNIS = process.env.WEBHOOK_GEHEIMNIS || 'test-geheimnis'
const gesehen = new Set() // en producción: base de datos
http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/webhook') { res.writeHead(404); return res.end() }
const teile = []
req.on('data', t => teile.push(t))
req.on('end', () => {
const roh = Buffer.concat(teile).toString('utf8')
const zeit = req.headers['x-zeit'] || ''
const erwartet = 'sha256=' + crypto.createHmac('sha256', GEHEIMNIS).update(zeit + '.' + roh).digest('hex')
const erhalten = String(req.headers['x-signatur'] || '')
const a = Buffer.from(erwartet), b = Buffer.from(erhalten)
const echt = a.length === b.length && crypto.timingSafeEqual(a, b)
const frisch = Math.abs(Date.now() / 1000 - Number(zeit)) < 300
if (!echt || !frisch) { res.writeHead(401); return res.end() }
const ereignis = JSON.parse(roh)
if (!gesehen.has(ereignis.id)) { gesehen.add(ereignis.id); console.log('neu:', ereignis.typ) }
res.writeHead(200); res.end('ok')
})
}).listen(3000)
El receptor comprueba firma y antigüedad, omite los identificadores conocidos y responde de inmediato. Para el funcionamiento real necesita además HTTPS, un límite de tamaño y una cola para el procesamiento posterior.
Dónde están extendidos los webhooks
Los servicios de pago, las plataformas de código y las herramientas de formularios comunican eventos mediante webhook. También el reconocimiento de visitantes puede hacerlo: cuando el sistema comunica una empresa que visita su página, esta llega directamente a una herramienta de su elección. Cómo conduce eso a un sistema de ventas lo muestra Llevar las visitas del sitio web al CRM. La alternativa mediante consulta la describe ¿Qué empresas visitan mi sitio web?. Qué datos fluyen en ese proceso lo explica ¿Cómo funciona el reconocimiento de empresas?.
Preguntas frecuentes
¿Qué diferencia hay entre webhook y API? En la API pregunta su sistema, en el webhook el remitente se anuncia por sí mismo. Ambos pueden combinarse.
¿Qué ocurre si mi receptor se cae? La mayoría de los remitentes repiten la entrega durante cierto tiempo. Cuánto tiempo y con qué frecuencia figura en la documentación del proveedor. Para las lagunas debería prever una conciliación mediante la API.
¿Basta con comprobar la dirección del remitente? No. Las direcciones de remitente pueden falsificarse y cambian. Solo es fiable la firma con un secreto que solo usted y el remitente conocen.