Recibir un webhook parece trivial: una ruta, un POST, un JSON. Tal vez por eso los mismos errores aparecen en casi toda integración — y casi siempre se manifiestan en producción, en el peor momento posible: un pago aprobado que no libera el producto, un cobro procesado dos veces, eventos que desaparecen en silencio. Este artículo reúne los diez errores que más rompen integraciones de webhooks y la corrección directa para cada uno.
Si todavía te estás familiarizando con el concepto, conviene empezar por la guía definitiva sobre webhooks — este artículo asume que ya conoces el flujo básico evento → POST → respuesta.
1. Procesar todo antes de responder
El error clásico: el handler recibe el evento, genera la factura, llama a dos APIs externas, envía el correo de confirmación… y recién entonces responde 200. El problema es que los proveedores imponen timeouts cortos — en general entre 5 y 30 segundos. Si la respuesta no llega a tiempo, la entrega se marca como fallida y el evento se reenvía, ahora compitiendo con el procesamiento anterior que sigue en curso. Bajo carga, esto se convierte en una cascada de reintentos procesando el mismo trabajo en paralelo.
Corrección: el handler hace lo mínimo — valida la firma, persiste el evento crudo, lo encola — y responde de inmediato. El trabajo pesado ocurre en un worker, a tu propio ritmo:
app.post("/webhooks/pagos", async (req, res) => {
if (!firmaValida(req)) return res.status(401).end();
await cola.add("procesar-webhook", {
eventId: req.body.id,
payload: req.body,
});
res.status(200).end(); // confirma ya; el worker hace el resto
});Un detalle que vale el esfuerzo: persiste el evento crudo antes de encolarlo. Si el worker falla, reprocesas a partir de lo que quedó guardado — sin depender de que el proveedor lo reenvíe. La base de datos se vuelve tu búfer de seguridad, y el costo es un INSERT.
2. No validar la firma
Un endpoint de webhook es una URL pública que ejecuta lógica de negocio. Sin validación de firma, cualquier persona que descubra (o adivine) la URL puede enviar un POST falsificado con "type": "payment.approved" — y tu aplicación va a entregar el producto gratis. Esto pasa porque la validación se ve como "algo para después", y el después nunca llega.
Corrección: valida el HMAC que el proveedor envía en el header, con comparación timing-safe, y rechaza con 401 lo que no coincida. El mecanismo completo — incluida la protección contra replay — está explicado en Seguridad de webhooks: firma HMAC y buenas prácticas.
3. No ser idempotente
Los reintentos existen para garantizar la entrega al menos una vez — lo que implica que el mismo evento puede llegar dos, tres, diez veces. Basta con que tu respuesta 200 se pierda en la red para que el proveedor reenvíe un evento que ya procesaste. Sin idempotencia, el resultado es un cobro duplicado, correos por partida doble, inventario descontado dos veces.
Corrección: usa el id del evento como clave única y descarta los duplicados en la puerta de entrada:
INSERT INTO webhook_events (event_id, type, payload)
VALUES ($1, $2, $3)
ON CONFLICT (event_id) DO NOTHING;
-- 0 filas insertadas = duplicado: responde 200 y terminaResponder 200 al duplicado es intencional: el evento ya fue recibido; que lo reenvíen otra vez no ayuda a nadie.
4. Confiar en el orden de los eventos
La red no preserva el orden — y los reintentos lo empeoran. Si el primer intento de order.created falla y el reintento recién sale dos minutos después, el order.updated siguiente llega antes. El código que asume que "created siempre llega primero" se rompe de formas difíciles de reproducir.
Corrección: dos estrategias que funcionan bien juntas. Compara el timestamp del evento con el estado que ya tienes guardado e ignora los eventos más antiguos que el estado actual. O trata el webhook solo como una señal de "algo cambió en este recurso" y busca el estado actual en la API del proveedor — la respuesta de la API es, por definición, el estado más reciente.
Un ejemplo concreto de cómo muerde esto: el cliente paga y cancela enseguida. Los eventos salen en el orden correcto — payment.approved, después payment.cancelled — pero el primero falla en la entrega y el reintento lo hace llegar después de la cancelación. El código que aplica cada evento al llegar termina con un pago cancelado marcado como aprobado. Comparar timestamps habría descartado el evento atrasado.
5. Tratar el webhook como fuente de verdad
Liberar acceso, descontar inventario o marcar una factura como pagada basándote solo en el contenido del POST recibido es apostar a que ese payload es legítimo, actual y completo. Incluso con una firma válida, el evento puede estar desactualizado (un reembolso pudo haber ocurrido después) o reflejar un estado intermedio.
Corrección: el webhook notifica; la API confirma. Al recibir "pago aprobado", consulta el pago en la API del proveedor y decide con base en esa respuesta. El costo es una llamada más; el beneficio es no liberar nunca nada valioso con base en un aviso.
6. Ignorar la política de reintentos del proveedor
El status code que respondes es una conversación con el mecanismo de reintentos del proveedor — y muchas integraciones responden lo equivocado. Los dos errores simétricos: responder 500 ante un error permanente de negocio ("pedido no encontrado"), generando días de reintentos inútiles y alertas; y responder 200 con la base de datos caída, diciéndole al proveedor que todo está bien — ese evento no vuelve nunca más.
| Situación | Respuesta correcta | Efecto |
|---|---|---|
| Evento recibido y aceptado (aunque el negocio lo ignore) | 200 | El proveedor cierra la entrega |
| Firma inválida, payload malformado | 400 / 401 | Error permanente — reintentar no lo resuelve |
| Falla transitoria (base de datos caída, cola llena) | 500 | El proveedor reintenta con backoff — es lo que quieres |
Corrección: clasifica las fallas de tu handler entre permanentes y transitorias y devuelve el código que produce el comportamiento de reintento que necesitas.
Vale la pena automatizar la clasificación: las excepciones de validación se vuelven 4xx, las de infraestructura se vuelven 5xx, y el caso "no sé" se vuelve 5xx — ante la duda, es mejor recibir el evento de nuevo que perderlo para siempre.
7. Parsing frágil del payload
Tres variaciones del mismo error. Validación demasiado estricta, que rechaza el payload cuando el proveedor agrega un campo nuevo — y los proveedores agregan campos sin aviso; para ellos no es un breaking change. Asumir el content-type, cuando hay proveedores que envían application/x-www-form-urlencoded o JSON dentro de un campo de formulario. Y el más traicionero: validar la firma sobre el JSON re-serializado — JSON.stringify(JSON.parse(body)) cambia espacios y orden de claves, y la firma no coincide nunca más.
Corrección: calcula el HMAC siempre sobre el raw body (los bytes originales de la solicitud, antes de cualquier parseo — detalles en nuestra guía de seguridad), ignora los campos desconocidos en vez de rechazarlos y lee el Content-Type en vez de presuponerlo.
8. Endpoint sin HTTPS o con el secreto en la URL
Los payloads de webhook llevan correos de clientes, montos, identificadores de pago — tráfico que no puede viajar en texto plano. Y el atajo común de "poner un token en la URL" (/webhooks?secret=abc123) como única autenticación es frágil: las URLs aparecen en logs de proxy, herramientas de monitoreo e historiales de configuración, y el secreto se filtra con ellas.
Corrección: HTTPS obligatorio (los proveedores serios ni siquiera aceptan una URL http://), el secreto fuera de la URL y la autenticidad garantizada por la firma HMAC, con rotación periódica del secreto.
9. No monitorear las entregas
Los webhooks fallan en silencio: no hay un usuario frente a la pantalla para quejarse del error. El escenario típico es descubrir días después que los pedidos dejaron de sincronizarse — y algunos proveedores (GitHub y Stripe, por ejemplo) desactivan automáticamente los endpoints que fallan de forma persistente, convirtiendo un bug temporal en una interrupción permanente.
Corrección: registra cada entrega (evento, status respondido, latencia), alerta cuando la tasa de fallas suba y ten un camino de replay/reprocesamiento para ponerte al día después de un incidente. Una herramienta de inspección de webhooks ayuda a ver en la práctica qué está llegando cuando los números no cierran.
Lo mínimo que vale la pena registrar por entrega: id del evento, tipo, hora de llegada, status que respondiste y tiempo de procesamiento. Con esos cinco campos respondes las preguntas que importan en un incidente — "¿llegó el evento?", "¿lo aceptamos?", "¿cuándo empezó a subir la tasa de fallas?" — sin depender del panel del proveedor.
10. No tener un entorno de pruebas
Registrar la URL de producción y "ver si funciona" es el camino más corto para procesar eventos reales con código roto — y para contaminar datos de producción con pruebas. El motivo es casi siempre el mismo: los webhooks son fastidiosos de probar localmente, así que la prueba se salta.
Corrección: usa los eventos de prueba del propio proveedor (Stripe CLI, simulador de Mercado Pago, redelivery de GitHub), inspecciona el payload real con un endpoint de inspección antes de escribir el parser y desarrolla contra tu localhost con túnel o reenvío — el paso a paso completo está en Cómo probar webhooks en localhost.
Resumen
- Responde rápido y procesa en una cola; el timeout del proveedor no espera a que tu correo se envíe.
- Valida la firma sobre el raw body y trata el endpoint como la puerta pública que es.
- La idempotencia no es opcional: el mismo evento va a llegar más de una vez.
- El orden y el contenido del evento son pistas, no verdad — confirma el estado en la API.
- El status code correcto para cada falla, logs por entrega y replay salvan la integración en el primer incidente.