Son las 3:14 de la madrugada cuando salta la alerta: un cliente fue cobrado dos veces por la misma orden. El proveedor de pagos disparó el evento de "pago exitoso" cuatro veces seguidas, y el handler que alguien configuró en veinte minutos, viendo llegar el primer payload sin problemas en los logs, las procesó las cuatro.
Esa parte nunca aparece en la guía de inicio rápido. Los webhooks son una de las ideas más simples en sistemas distribuidos: cuando pasa algo, llamá a esta URL. Pero la distancia entre "llamar a esta URL cuando pasa algo" y "construir algo que sobreviva al internet real" es enorme, y casi nadie la cierra hasta que algo se rompe en producción.
La garantía de entrega que nadie te prometió
Esto es lo que la mayoría de los equipos hace mal el primer día: asumen entrega "al menos una vez" cuando en realidad están construyendo para "exactamente una vez", y no notan el desajuste hasta que les cuesta dinero. Cualquier proveedor serio de webhooks, Stripe, GitHub, Shopify, el que sea, documenta que los eventos pueden llegar más de una vez y de hecho van a llegar más de una vez. Cortes de red, timeouts de tu lado, reintentos del proveedor después de un error 5xx, un balanceador de carga que mata una conexión a mitad de respuesta. Todo eso se ve igual desde el lado de quien envía: no llegó confirmación, así que se reenvía.
Si tu handler trata cada webhook entrante como un evento nuevo que nunca vio antes, no tienes una integración de webhooks. Tienes un sistema que en algún momento va a procesar dos veces algo importante, y suele ser dinero, inventario, o una notificación que sale duplicada y hace que tu producto se vea roto.
La solución no es complicada en teoría. Cada evento tiene un ID. Guardas qué IDs ya procesaste. Revisas antes de actuar. En la práctica eso significa una tabla extra, una restricción de unicidad, y una decisión sobre cuánto tiempo guardas ese historial, porque "para siempre" no es gratis y "24 horas" puede no ser suficiente si el proveedor decide reintentar una entrega fallida una semana después. Stripe, como referencia, reintenta ciertos webhooks fallidos durante hasta tres días. Diseña tu ventana de deduplicación para esa realidad, no para el caso feliz que probaste en desarrollo.
El orden es una promesa que casi ningún proveedor hace
La segunda suposición que rompe cosas en silencio es el orden. Construiste tu handler asumiendo que el evento de "orden creada" llega antes que el de "orden enviada", porque así fue que pasaron los hechos. El problema es que las requests HTTP no se ponen en fila educadamente. Dos eventos disparados con sesenta milisegundos de diferencia pueden llegar a tu servidor en cualquier orden, sobre todo cuando corres más de una instancia detrás de un balanceador, o cuando un reintento queda encolado detrás de un evento más reciente.
La mayoría de los proveedores lo dicen explícitamente en su documentación, y la mayoría de las integraciones ignora la advertencia igual, porque construir un handler que no dependa del orden es más trabajo que construir uno que asume una línea de tiempo. Las opciones honestas son: diseñar tu máquina de estados para que los eventos fuera de orden sean seguros (una orden que ya se marcó como enviada simplemente ignora un evento tardío de "orden creada"), o volver a consultar el estado actual en la API de origen en lugar de confiar en que el payload cuenta toda la historia. Las dos cuestan más por adelantado que "procesar el evento como llega". Las dos te evitan el reporte de bug que dice que el sistema muestra una orden creada después de que se envió, algo genuinamente difícil de explicar a alguien que no es técnico.
Verificar la firma no es opcional, y saltearlo es más común de lo que parece
Tu endpoint de webhook es una URL pública que dispara acciones reales en tu sistema cuando recibe un POST. Si no verificas que la request realmente viene del proveedor, construiste una API que cualquiera en internet puede llamar para hacerle creer a tu sistema que un pago se completó, que se creó una cuenta, o que un envío salió.
Todo proveedor que se toma esto en serio firma sus payloads con una firma HMAC en un header, y la documentación de cada proveedor explica cómo verificarla. La razón por la que esto sigue apareciendo como un hueco en sistemas de producción no es ignorancia. Es que verificar la firma se siente como una formalidad cuando estás probando localmente con curl y un payload hardcodeado, así que queda comentado "por ahora", y el ticket para agregarlo nunca se prioriza una vez que la feature sale y funciona.
Los ingenieros que hacen esto bien tratan la verificación de firma como parte del endpoint, no como una mejora sobre él. No existe una versión "terminada" de un handler de webhooks sin eso, de la misma manera que no existe una versión "terminada" de un login sin verificar la contraseña.
Los reintentos van a sobrevivir tus suposiciones sobre el tiempo
Los proveedores no se rinden después de un intento fallido. Reintentan, generalmente con backoff exponencial, a veces durante días. Eso significa que tu handler tiene que sobrevivir a que lo llamen por un evento que pasó el martes pasado, con un estado que cambió por debajo mientras tanto. Una orden que se canceló después de que se disparó el webhook pero antes de que llegara el reintento. Un usuario que se borró entre el primer intento y el quinto.
Ahí es donde la idempotencia y el orden dejan de ser preocupaciones separadas y empiezan a combinarse. Un handler idempotente que asume que el estado actual coincide con el timestamp del payload va a seguir fallando. Los incidentes reales casi nunca los causa la primera entrega. Los causa el tercer reintento, cuatro días después, golpeando un sistema que ya siguió adelante sin avisarle a nadie.
Los timeouts son un contrato que no leíste
Los proveedores esperan que tu endpoint responda rápido, generalmente en pocos segundos, y cuentan un timeout como un fallo que vale la pena reintentar. Si tu handler hace trabajo real de forma sincrónica (actualizar una base de datos, llamar a una API downstream, enviar una notificación) antes de devolver un 200, estás apostando a que todo eso termine dentro de una ventana que no controlas.
El patrón que funciona: confirmar la recepción de inmediato, y hacer el trabajo real de forma asíncrona en una cola. Eso agrega una pieza más al sistema, que es exactamente por qué muchos equipos lo saltean al principio. También es por qué esos mismos equipos terminan debugueando procesamiento duplicado misterioso más adelante, porque un handler sincrónico lento que hace timeout se ve, del lado del proveedor, exactamente igual a un handler que nunca recibió el mensaje. Así que reintenta. Y vuelves al primer problema, solo que esta vez lo causaste tú mismo.
El costo de hacer esto mal se acumula en silencio
Lo peligroso de un handler de webhooks mal diseñado es que puede correr durante meses sin que nadie lo note. El procesamiento duplicado no tira abajo el servidor. Cobra dos veces a un cliente de cada diez mil, o dispara una notificación duplicada una vez cada cien mil, y a menos que alguien esté buscando duplicados específicamente, la primera señal de que algo anda mal es un ticket de soporte que parece un caso aislado en vez de un patrón.
Para cuando alguien conecta los puntos, suele haber un backlog de registros afectados sin una forma clara de distinguir cuáles se duplicaron realmente y cuáles se procesaron dos veces por otra razón legítima. Desenredar eso después de los hechos toma mucho más tiempo del que hubiera tomado construir la validación desde el principio. Esta es la parte que convierte la confiabilidad de los webhooks en un tema genuinamente senior y no en un checkbox: la falla no se anuncia, se acumula.
El polling sigue siendo peor: más lento, más caro a escala, y no elimina ninguno de estos problemas, solo los esconde. Antes de dar por cerrado un handler de webhooks, esto tiene que estar resuelto:
— Deduplicación por ID de evento, con una ventana pensada para reintentos de días, no de minutos.
— Un diseño que no dependa del orden de llegada, o una relectura del estado real contra la API de origen.
— Verificación de firma HMAC tratada como parte del endpoint, no como una mejora futura.
— Confirmación inmediata de recepción y procesamiento real en una cola asíncrona.
— Un método para detectar duplicados que no dependa de que un ticket de soporte llegue primero.
Eso es la tarea real detrás de las diez líneas de código que reciben el payload.




