Objetivos de aprendizaje
Después de leer este artículo podrás:
- Entender qué es un webhook y en qué se diferencia de consultar una API
- Recibir un webhook y verificar su firma HMAC en Node.js
- Evitar los errores típicos: respuestas lentas, eventos duplicados y URLs sin protección
Imagina que vendes un producto y quieres activar la cuenta del cliente en cuanto pague. Puedes preguntarle a la pasarela de pago cada diez segundos si ya llegó el pago, o puedes pedirle que te avise cuando suceda. Esa segunda opción es un webhook.
Los webhooks están en casi todo: pagos, notificaciones de GitHub, mensajes de bots de Telegram, formularios, alertas de monitoreo. Entender cómo funcionan te ahorra peticiones inútiles y te evita algunos problemas de seguridad.
¿Cómo funciona un webhook?#
El flujo tiene tres pasos:
- Tú registras una URL en el servicio emisor (por ejemplo
https://tuapp.com/webhooks/pagos) y eliges qué eventos quieres recibir. - Cuando ocurre uno de esos eventos, el servicio envía una petición HTTP
POSTa tu URL. El cuerpo suele ser JSON con el tipo de evento y los datos. - Tu servidor responde con un código
2xxpara confirmar que lo recibió.
Si no respondes con 2xx, el emisor lo considera una entrega fallida. Stripe, por ejemplo, trata también las redirecciones 3xx como fallos.
Un evento típico se ve así:
{
"id": "evt_123",
"type": "payment.succeeded",
"data": { "order_id": "ped_42", "amount": 4990 }
}Los nombres de los campos cambian de un servicio a otro. Lo que se mantiene es la idea: un POST con un identificador de evento, un tipo y los datos.
¿Qué diferencia hay entre webhook y polling?#
Con polling tu app pregunta de forma periódica si hay algo nuevo. Es simple, pero la mayoría de las respuestas son "nada nuevo": gastas peticiones y te enteras de cada evento con un retraso de hasta un intervalo completo.
Con un webhook el aviso llega cuando el evento ocurre, y solo entonces. Hay menos tráfico y menos latencia. A cambio, tu servidor tiene que estar accesible desde internet y preparado para recibir peticiones de terceros.
En la práctica se combinan: el webhook avisa, y si sospechas que perdiste uno, una tarea periódica consulta la API REST para reconciliar.
¿Cómo recibo un webhook en Node.js?#
Este ejemplo usa Express 5 con módulos ES ("type": "module" en el package.json) y verifica la firma del evento. El esquema es el que usa GitHub: un HMAC-SHA256 del cuerpo, calculado con un secreto compartido y enviado en la cabecera X-Hub-Signature-256 con el prefijo sha256=. Otros servicios usan nombres de cabecera y formatos distintos (Stripe usa Stripe-Signature, que además incluye una marca de tiempo), así que revisa siempre su documentación.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.WEBHOOK_SECRET;
function firmaValida(cuerpo, cabecera) {
// si el Content-Type no coincide, express.raw deja req.body sin definir
if (!cabecera || !Buffer.isBuffer(cuerpo)) return false;
const esperada = Buffer.from(
"sha256=" + createHmac("sha256", SECRET).update(cuerpo).digest("hex")
);
const recibida = Buffer.from(cabecera);
// timingSafeEqual lanza un error si las longitudes difieren
return recibida.length === esperada.length && timingSafeEqual(recibida, esperada);
}
// express.raw conserva el cuerpo exacto en bytes, necesario para la firma
app.post("/webhooks/pagos", express.raw({ type: "application/json" }), (req, res) => {
if (!firmaValida(req.body, req.get("X-Hub-Signature-256"))) {
return res.sendStatus(401);
}
const evento = JSON.parse(req.body.toString("utf8"));
console.log("Evento recibido:", evento.type);
res.sendStatus(200); // responde primero, procesa después
});
app.listen(process.env.PORT ?? 3000);Para probarlo sin depender de un servicio externo, calcula la firma tú mismo:
CUERPO='{"id":"evt_123","type":"payment.succeeded"}'
FIRMA="sha256=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $NF}')"
curl -i -X POST http://localhost:3000/webhooks/pagos \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: $FIRMA" \
-d "$CUERPO"Si cambias una sola letra del cuerpo sin recalcular la firma, recibirás 401.
¿Por qué hay que verificar la firma?#
Tu URL es pública. Sin verificación, cualquiera que la conozca puede enviar un evento falso de "pago aprobado" y que tu sistema active una cuenta sin cobrar. La documentación de Stripe advierte exactamente de eso.
Tres reglas para verificar la firma:
- Usa el cuerpo en crudo. Si un middleware convierte el JSON a objeto y lo vuelve a serializar, los bytes cambian y la firma deja de coincidir. Stripe lo marca como causa frecuente de fallos de verificación.
- Compara en tiempo constante. GitHub recomienda
crypto.timingSafeEqualen lugar de==para evitar ataques de temporización. - Guarda el secreto en una variable de entorno, nunca en el código ni en el repositorio.
Algunos emisores incluyen una marca de tiempo dentro de lo firmado. Stripe lo hace y sus librerías rechazan eventos con más de cinco minutos de diferencia por defecto, lo que frena los ataques de repetición. Como capa adicional, algunos emisores publican una lista de IPs de origen que puedes permitir en tu firewall.
¿Qué errores son comunes al trabajar con webhooks?#
Tardar demasiado en responder. Si haces el trabajo pesado antes de contestar, el emisor puede dar la entrega por fallida por tiempo agotado y reenviar. Stripe recomienda devolver el 2xx antes de cualquier lógica compleja y procesar con una cola asíncrona.
Procesar el mismo evento dos veces. Los emisores pueden entregar un evento más de una vez, sobre todo cuando reintentan. Guarda el identificador de cada evento procesado y descarta los repetidos. Así tu handler es idempotente.
Asumir un orden. Stripe indica expresamente que no garantiza entregar los eventos en el orden en que se generaron. Si necesitas el estado actual, consúltalo en la API en vez de fiarte del orden de llegada.
Aceptar todos los tipos de evento. Suscríbete solo a los que usas. Menos eventos, menos carga y menos superficie de error.
Probar solo en producción. Usa un túnel como ngrok o la CLI del emisor, y revisa el panel de entregas, donde suelen verse el código de estado y el momento del siguiente reintento.
¿Cuándo no conviene usar un webhook?#
Un webhook necesita un servidor accesible y siempre disponible. Si tu cliente es una página web o una app móvil que necesita datos en vivo, una conexión persistente como un WebSocket encaja mejor. Si el emisor no ofrece webhooks, o si necesitas leer el estado completo cada cierto tiempo, el polling sigue siendo válido.
Cuando perder un evento sea costoso, no dependas solo del webhook: combínalo con una reconciliación periódica por API.
Cuidado también con el nombre. Los webhooks de Discord son una URL para publicar mensajes en un canal: ahí tú envías, mientras que aquí recibes.
Cómo hacerlo en Square Cloud#
Un webhook es, en el fondo, una ruta HTTP de tu aplicación web con una URL pública y HTTPS. Una aplicación web en Square Cloud recibe un subdominio *.squareweb.app con HTTPS en cualquier plan, así que puedes registrar https://tu-app.squareweb.app/webhooks/pagos en el emisor sin montar un proxy ni certificados.
Pasos para el ejemplo de arriba:
- Despliega la aplicación Node.js desde el panel, la CLI, GitHub o la extensión de VS Code. El archivo de configuración
squarecloud.appes opcional: el panel lo genera durante el deploy, o puedes escribirlo en la raíz del proyecto. - Define
WEBHOOK_SECRETen las variables de entorno de la aplicación. El cambio se aplica en el siguiente reinicio, sin rebuild. - Registra la URL pública en el emisor e imprime un log por cada entrega en tu código: los logs en tiempo real del panel muestran lo que tu aplicación imprime.
Las aplicaciones están siempre encendidas, sin modo de suspensión. Eso importa porque un emisor que no obtiene respuesta marca la entrega como fallida. Los bots de Telegram también pueden recibir actualizaciones por webhook en vez de long polling, con un subdominio HTTPS.
Consulta la página de hosting de APIs para ver runtimes, planes y límites.
Hosting de APIs
APIs en contenedores siempre encendidos, sin cold start
Preguntas frecuentes
¿Cuál es la diferencia entre un webhook y una API?
En una API tú haces la petición cuando quieres datos. En un webhook es el otro servicio quien te llama cuando algo sucede. Se complementan: el webhook avisa y la API sirve para pedir los detalles.
¿Un webhook necesita HTTPS?
En producción, sí. Stripe, por ejemplo, exige una URL HTTPS pública para los endpoints de producción. Además, HTTPS evita que el contenido del evento se lea por el camino.
¿Qué pasa si mi servidor está caído cuando llega un webhook?
Depende del emisor. Muchos reintentan: Stripe reintenta hasta tres días con espera exponencial en producción. Otros no reintentan, así que conviene revisar su documentación y poder reconciliar con la API.
¿Cómo pruebo un webhook en mi computadora?
Necesitas una URL pública que llegue a tu máquina. Puedes usar un túnel como ngrok, o la herramienta del emisor, como `stripe listen`, que reenvía los eventos a localhost.
¿Es lo mismo un webhook que un WebSocket?
No. Un webhook es una petición HTTP puntual del emisor hacia tu servidor. Un WebSocket es una conexión persistente y bidireccional que abres entre cliente y servidor.
Más en APIs
Ver los 15 artículos- Cómo hospedar una APIHospedar una API es ejecutar su servidor HTTP las 24 horas en una dirección pública con HTTPS. Guía paso a paso con Node.js y Python, pruebas y errores comunes.
- Cómo hospedar una aplicación Node.jsPara hospedar una aplicación Node.js, prepara el package.json, lee el puerto y los secretos del entorno, elige una versión LTS y ejecútala con node. Paso a paso.
- Cómo hospedar FastAPI, Flask o DjangoPara hospedar FastAPI, Flask o Django, ejecuta la aplicación con un servidor de producción (Uvicorn o Gunicorn) en 0.0.0.0. Comandos, ajustes de Django y errores.
- Cómo hospedar una app o script de PythonPara hospedar una app o script de Python, fija la versión, aísla las dependencias en un venv, lee los secretos del entorno y maneja SIGTERM. Guía paso a paso.