4dim / Notas
El webhook de WhatsApp responde 200 aunque todo falle
Meta reintenta durante horas lo que no recibe un 200 y termina desactivando el webhook. De ahí salen seis reglas: firmar sobre los bytes, deduplicar en la base, no llamar a Meta desde dentro, y resolver la bandeja por número.
Meta reintenta durante horas
Si tu negocio recibe mensajes de WhatsApp en un sistema propio, o vas a contratar uno, esta nota explica la pieza de la que depende todo: el webhook. Es la dirección a la que Meta manda cada mensaje que un cliente te escribe. Si esa dirección falla, no falla un mensaje. Fallan todos los que vienen detrás.
Meta espera que el webhook responda «200», el código que significa «recibido». Si responde otra cosa, Meta asume que el mensaje no llegó y lo vuelve a mandar. Sigue reintentando durante horas. Y si los fallos continúan, desactiva el webhook. A partir de ahí no entra nada de nadie.
De ahí sale una regla que parece al revés. El webhook debe responder 200 siempre. Aunque la base de datos haya fallado. Aunque la foto adjunta no se haya podido descargar. El error se anota para revisarlo después, pero a Meta se le dice que llegó. Un mensaje mal procesado es un problema de un mensaje. Un webhook que responde 500, el código de «error del servidor», es un problema de todos los mensajes que vienen detrás.
El webhook no es el sitio donde se decide si algo salió bien. Es el sitio donde se recibe. Lo demás se resuelve después, y con calma.
La firma se calcula sobre los bytes, no sobre el JSON
Cada llamada de Meta llega firmada. La firma es un código calculado sobre el contenido exacto que Meta envió, y sirve para comprobar que la llamada viene de Meta y no de cualquiera que conozca la dirección. Para verificarla, el servidor calcula el mismo código por su cuenta y compara.
El error clásico está en el orden. Muchos sistemas leen primero el mensaje como JSON, lo vuelven a convertir a texto y firman eso. El texto resultante tiene otros espacios y otro orden de claves. La firma ya no coincide, y el webhook rechaza mensajes legítimos sin que nada explique por qué.
Lo correcto es leer el cuerpo como texto crudo, verificar la firma sobre esos bytes, y solo entonces interpretarlo. La comparación se hace con una función de tiempo constante, que tarda lo mismo acierte o no. Una comparación normal se detiene en el primer carácter distinto, y ese tiempo de respuesta le dice a un atacante cuántos caracteres acertó.
Hay una verificación más, la primera de todas. Al configurar el webhook, Meta manda una llamada de prueba con un hub.challenge, un reto que hay que devolver tal cual. Solo lo devolvemos si el token de verificación coincide; si falta, respondemos error. Un webhook que acepta cualquier verificación es un webhook que cualquiera puede registrar.
El mismo mensaje llega dos veces
Como Meta reintenta, el mismo mensaje puede llegar dos veces. Y si el sistema corre en dos procesos, el reintento puede caer en un proceso distinto mientras el primero todavía está guardando el original.
Preguntar «¿ya tengo este id?» antes de guardar no sirve. Los dos procesos preguntan a la vez, los dos reciben «no», y los dos guardan. El mensaje aparece duplicado en la conversación.
La deduplicación tiene que vivir en la base de datos: una regla que impide guardar dos mensajes con el mismo identificador de Meta. El segundo intento falla, se ignora, y queda un solo mensaje. La base es la única capa que ve los dos intentos.
Qué no hacer dentro del webhook
El tiempo que tarda el webhook en responder es lo que decide si Meta reintenta. Por eso dentro de él no cabe nada lento.
- No llamar a Meta desde el webhook. Tentaría pedir la foto de perfil de quien escribe por Messenger en ese mismo momento. Sería una llamada a Meta dentro de la respuesta a Meta. Se deja para cuando alguien abra el contacto.
- Los adjuntos sí se bajan, pero con tope. En WhatsApp, la dirección temporal de un archivo vive minutos, así que no se puede posponer la descarga. Se hace dentro del webhook con un plazo de 8 segundos: sobra para una foto y no alcanza para un video largo. Si no alcanza, el mensaje entra igual, con un texto que dice que el archivo no se pudo traer. Perderlo en silencio es peor que enseñarlo a medias.
- Lo que no se entiende, se guarda. Una ubicación, un contacto compartido o un tipo de mensaje nuevo entran con un aviso: «[location recibido, todavía no se ve aquí]». El cliente escribió. Que el software no sepa pintarlo no es motivo para que desaparezca.
Quién es quién: el número, no el negocio
Un negocio puede tener dos números de WhatsApp, por ejemplo Ventas y Posventa. Los mensajes de ambos llegan al mismo webhook. Para saber a qué bandeja va cada mensaje, la única llave fiable es el phone_number_id, el identificador del número que recibió el mensaje, que viene en cada evento.
Durante un tiempo nuestro webhook elegía la bandeja por tipo de canal. Como los dos números eran «WhatsApp», un mensaje enviado a Ventas aparecía en el hilo de Posventa. Sin ningún error. Hoy se busca la conexión por ese identificador exacto. Un phone_number_id desconocido se anota como aviso y se ignora: suele ser un resto de una cuenta de pruebas.
Lo que Meta cuenta del remitente es poco. En WhatsApp llega el nombre del perfil, el wa_id, que es el número, y un user_id que no sirve para pedir nada. Lo comprobamos con dieciséis peticiones y lo contamos en otra nota.
Qué hacer entonces
- Responder 200 siempre. Anotar el error por dentro.
- Leer el cuerpo crudo, verificar la firma sobre él, y solo después interpretarlo.
- Deduplicar con una regla de unicidad en la base, no con una pregunta en el código.
- Resolver la bandeja por
phone_number_id, no por tipo de canal. - No hacer nada lento dentro: ni llamadas a Meta, ni descargas sin plazo. Dejar un texto de respaldo cuando algo no se pudo traer.
- Rechazar la verificación inicial si falta el token configurado.
Esta nota sale de nuestro trabajo en AI-CRM Connection.