Fundamentos de webhook
La WhatsApp Cloud API entrega eventos a tu aplicación a través de un sistema de webhook basado en push. En lugar de consultar un endpoint en busca de nuevos eventos, los servidores de Meta envían mediante POST un payload JSON a una URL que registras — tu endpoint de webhook. Tu servidor procesa el payload y devuelve HTTP 200 para confirmar la recepción.
Dos métodos HTTP operan en la misma URL:
- GET — desafío de verificación único durante el registro. Tu endpoint debe devolver el parámetro de consulta
hub.challengecomo texto plano. - POST — notificaciones de eventos en vivo. Cada mensaje entrante, actualización de estado y evento de cuenta llega aquí.
Las restricciones más importantes que debes internalizar antes de escribir cualquier código:
- Responder dentro de 20 segundos — cualquier cosa más lenta desencadena un reintento. Confirma inmediatamente, procesa de forma asíncrona.
- Entrega at-least-once — el mismo evento puede llegar más de una vez. Tu handler debe ser idempotente.
- Sin garantía de orden — los eventos pueden llegar fuera de secuencia. Nunca asumas orden cronológico.
- Límite de tamaño de payload de 3 MB — los payloads individuales de webhook no excederán 3 MB. Los archivos multimedia no se incluyen en línea.
- HTTPS requerido — Meta rechaza endpoints HTTP simples. Se requiere certificado SSL válido.
Referencia completa de propiedades de webhook
| Propiedad | Valor / Especificación | Notas |
|---|---|---|
| Protocolo | Solo HTTPS | Se requiere certificado SSL válido. HTTP rechazado por completo. |
| Dirección | Proveedor → Consumidor (push) | Meta hace push hacia ti. No se requiere polling. |
| Método de verificación | GET + hub.verify_token | Único durante el registro. Devolver hub.challenge como texto plano. |
| Autenticación en eventos | X-Hub-Signature-256 HMAC-SHA256 | Firmado con App Secret. Verificar cada POST. |
| Método HTTP para eventos | POST | Siempre POST. Nunca GET para eventos en vivo. |
| Formato de datos | JSON | Content-Type: application/json |
| Timeout de respuesta | 20 segundos | Excederlo desencadena reintento. Devolver 200 inmediatamente. |
| Criterio de éxito | HTTP 200 | Cualquier no-200 desencadena mecanismo de reintento. |
| Límite de tamaño de payload | 3 MB | Medios no en línea — referenciados por ID. |
| Duración de reintento | Hasta 7 días | Backoff exponencial. Ver programa completo abajo. |
| Garantía de entrega | At-least-once | Duplicados posibles. Hacer handler idempotente. |
| Garantía de orden | Ninguna | Los eventos pueden llegar fuera de orden cronológico. |
| Niveles de webhook | Número de teléfono + WABA | El número de teléfono tiene prioridad sobre el fallback de WABA. |
| Replay manual | No soportado | Sin replay nativo. Eventos perdidos después de 7 días. |
| Algoritmo de firma | HMAC-SHA256 | Clave = App Secret. Entrada = bytes brutos del body. |
| Header de firma | X-Hub-Signature-256 | Formato: sha256=<hex> |
| Entregas concurrentes | Múltiples por segundo | Números de alto volumen reciben muchos eventos/segundo. |
Todos los campos de suscripción de webhook
Te suscribes a campos individuales en el Meta Developer Dashboard (WhatsApp → Configuration → Webhooks → Manage). Suscribirse a messages es obligatorio para la mayoría de las integraciones. Cada campo representa una categoría de eventos — suscríbete solo a lo que necesites para reducir el volumen de payload.
| Campo | Cobertura | Prioridad |
|---|---|---|
| messages | Todos los mensajes entrantes de clientes + todas las actualizaciones de estado salientes (sent / delivered / read / failed). El campo principal para cualquier integración de mensajería. | Suscribirse siempre |
| account_update | Violaciones de política (ACCOUNT_VIOLATION) y restricciones activas (ACCOUNT_RESTRICTION) en tus números de teléfono. Esencial para monitoreo en producción. |
Recomendado |
| message_template_status_update | Aprobación, rechazo, pausa y desactivación de templates. Se dispara cuando Meta cambia el estado de un template que has enviado. | Recomendado |
| phone_number_quality_update | Cambios en la calificación de calidad (GREEN / YELLOW / RED) para tus números de teléfono. La calidad afecta tu tier de mensajería. Suscribirse para detectar degradación temprano. | Recomendado |
| phone_number_name_update | Eventos de aprobación o rechazo del nombre para mostrar. Se dispara cuando Meta revisa un nombre que has enviado para tu número de WhatsApp Business. | Situacional |
| business_capability_update | Cambios de tier de límite de mensajería — cuando Meta mejora o degrada tu tier diario de conversaciones (1K / 10K / 100K / Unlimited). | Situacional |
| flows | Interacciones de WhatsApp Flows — eventos de envío de datos cuando un usuario completa o interactúa con un Flow adjunto a tu número. | Si usas Flows |
| security | Eventos de PIN de verificación en dos pasos. Se dispara cuando se cambia o desactiva el PIN de un número de teléfono. | Opcional |
| message_template_components_update | Se dispara cuando Meta modifica los componentes de un template aprobado (raro — Meta puede ajustar templates para cumplir con políticas). | Opcional |
| account_alerts | Alertas de facturación, advertencias de capacidad y otros avisos operativos a nivel de cuenta de Meta. | Opcional |
Eventos entregados por campo de suscripción
Campo messages — tipos de mensajes entrantes
El campo messages entrega dos categorías: mensajes entrantes de clientes (identificados por un array messages en el objeto value) y actualizaciones de estado de entrega salientes (identificadas por un array statuses). Verifica qué array está presente antes de procesar.
| msg.type | Ubicación del contenido | Notas |
|---|---|---|
| text | msg.text.body | Cuerpo de mensaje de texto plano. Puede incluir URLs. |
| image | msg.image.id, .mime_type, .caption | ID → resolver URL → descargar. Caption opcional. |
| audio | msg.audio.id, .voice | voice: true si se grabó en la app. Siempre descargar inmediatamente. |
| video | msg.video.id, .caption | Caption opcional. |
| document | msg.document.id, .filename, .mime_type | Filename incluido — guárdalo. |
| sticker | msg.sticker.id, .animated | El flag animated distingue WebP vs sticker animado. |
| location | msg.location.latitude, .longitude, .name, .address | Name y address son opcionales. |
| contacts | msg.contacts[n].name, .phones | Array — el cliente puede compartir múltiples contactos. |
| reaction | msg.reaction.emoji, .message_id | Referencia el ID del mensaje al que reaccionó el cliente. |
| interactive | msg.interactive.type, luego .button_reply o .list_reply | Verifica type antes de leer el sub-objeto. |
| order | msg.order.catalog_id, .product_items | WhatsApp Commerce — pedido de producto desde catálogo. |
| system | msg.system.body, .type | Eventos del sistema: cliente cambió de número, etc. |
| button | msg.button.text, .payload | Toque de botón de respuesta rápida desde un mensaje template. |
| referral | msg.referral.source_url, .source_type, .source_id | Datos de referencia de anuncio Click-to-WhatsApp junto al mensaje. |
Campo messages — actualizaciones de estado salientes
| valor de status | Significado | Campos adicionales |
|---|---|---|
| sent | Mensaje aceptado por Meta y reenviado a WhatsApp. Aún no entregado al dispositivo. | timestamp, recipient_id, conversation, pricing |
| delivered | El mensaje llegó al dispositivo del destinatario (doble tick gris). | timestamp, recipient_id, conversation, pricing |
| read | El destinatario abrió el chat (doble tick azul). Solo se dispara si las confirmaciones de lectura están activadas. | timestamp, recipient_id |
| failed | Entrega fallida permanentemente. Revisa status.errors[0].code para el error específico. | array errors con code y title |
Programa exacto de reintentos con tiempos de backoff
Meta reintenta entregas de webhook fallidas hasta por 7 días usando backoff exponencial. "Fallido" significa que tu endpoint devolvió no-200, tuvo timeout (tardó más de 20 segundos) o fue inalcanzable. Después de 7 días, el evento se descarta permanentemente — no existe ruta de recuperación nativa.
Comportamiento de códigos de respuesta HTTP
Lo que devuelves a Meta determina si el evento se considera entregado, reintentado o si desencadena una alerta. El árbol de decisiones es más simple de lo que la mayoría de desarrolladores espera — todo es binario: 200 significa éxito, cualquier otra cosa significa reintento.
Especificación completa de firma HMAC-SHA256
Cada solicitud POST de Meta incluye una firma criptográfica que te permite verificar que la solicitud genuinamente vino de Meta y no fue manipulada en tránsito. Omitir esta verificación significa que cualquier atacante que descubra tu URL de webhook puede alimentar datos arbitrarios a tu aplicación.
Formato del header
Verificación en Node.js
Verificación en Python
JSON.stringify(JSON.parse(body)) produce bytes diferentes al original — espacios en blanco, orden de claves y precisión de números pueden cambiar. Siempre calcula HMAC sobre los bytes exactos que llegaron en la solicitud HTTP, antes de cualquier transformación.Webhooks a nivel de WABA vs nivel de número de teléfono
La WhatsApp Cloud API soporta dos niveles de configuración de webhook que interactúan en un orden de prioridad específico. Entender esto previene la pérdida silenciosa de eventos al gestionar múltiples números de teléfono.
| Aspecto | Webhook de número de teléfono | Webhook de WABA |
|---|---|---|
| Ubicación de configuración | Meta Developer Dashboard → WhatsApp → Configuration | Meta Business Settings → WhatsApp Accounts → [WABA] → Webhook |
| Alcance | Un número de teléfono específico | Todos los números de teléfono en el WABA |
| Prioridad | Mayor — tiene precedencia | Menor — solo fallback |
| Comportamiento de fallback | Si está configurado, el webhook de WABA no recibe eventos para este número | Recibe eventos para números sin webhook de número de teléfono |
| Mejor para | Integraciones de un solo número, lógica de enrutamiento por número | Operaciones multi-número, agencia gestionando muchos números de clientes |
| Enrutamiento de eventos | Los eventos van solo a esta URL | Los eventos para todos los números no configurados llegan aquí |
| Campos de suscripción | Configurados por separado | Configurados por separado |
metadata.phone_number_id. Añade un webhook a nivel de número de teléfono solo para números que necesiten enrutamiento diferente. Esto es más limpio que mantener webhooks separados por número de cliente, y SocialHook soporta múltiples números por cuenta bajo un único stream de eventos normalizado.Entrega at-least-once — implicaciones y deduplicación
El sistema de webhook de Meta garantiza que un evento dado será entregado al menos una vez. No garantiza exactamente una vez. El escenario de duplicado: tu servidor procesa un evento, devuelve 200, pero la confirmación se pierde en tránsito antes de que el sistema de Meta la registre. Meta reintenta. Tu handler procesa el mismo evento nuevamente.
Para un bot de eco simple esto es intrascendente. Para un agente de IA que desencadena una acción de CRM, un pago o un mensaje saliente — los duplicados causan problemas reales. Tu handler debe ser idempotente.
Patrón de deduplicación
El valor msg.id (la cadena wamid.HBgL...) es único por mensaje. Las actualizaciones de estado usan el ID del mensaje saliente. Las reacciones y otros tipos de evento tienen sus propios IDs únicos. Siempre usa el ID específico del evento — nunca un valor derivado — como tu clave de deduplicación.
Esquemas de payload para tipos de evento clave
Envelope de nivel superior (todos los eventos)
Mensaje de texto entrante (objeto value)
Actualización de estado de mensaje saliente (objeto value)
Lista de verificación de seguridad
X-Hub-Signature-256 falte, esté malformado o no coincida. Sin esto, tu endpoint acepta eventos falsificados de cualquiera.crypto.timingSafeEqual() en Node.js, hmac.compare_digest() en Python. La igualdad simple de cadenas (=== / ==) filtra información de timing que los atacantes pueden explotar para falsificar firmas carácter por carácter.express.raw() en la ruta de webhook. En FastAPI: await request.body() antes de cualquier deserialización JSON.process.env.WHATSAPP_APP_SECRET (Node.js) o os.environ["WHATSAPP_APP_SECRET"] (Python). Rotar inmediatamente si se filtra.msg.id procesados en Redis con TTL de 24h+. Verificar antes de procesar. Omitir duplicados silenciosamente — nunca generar error por ellos.crypto.randomBytes(32).toString('hex').Errores comunes y soluciones
| Error / Síntoma | Causa raíz | Solución |
|---|---|---|
| Verificación falla en registro | Devolver JSON en lugar de texto plano, verify token incorrecto, o devolver hub.challenge envuelto en un objeto JSON |
Devolver hub.challenge como texto plano con Content-Type: text/plain. Cadena exacta — sin wrapping JSON. |
| HMAC siempre falla | Calcular HMAC después del parsing JSON, usar access token en lugar de App Secret, doble codificación del body | Usar express.raw() en Express. Llamar await request.body() en FastAPI antes de parsear. Clave = App Secret desde App Settings → Basic. |
| Los eventos no llegan en absoluto | Campo messages no suscrito, o webhook no guardado/verificado |
Dashboard → WhatsApp → Configuration → Webhooks → Manage → habilitar campo messages. Confirmar que el webhook muestra como verificado. |
| Recibir el mismo evento múltiples veces | Entrega at-least-once — comportamiento esperado, no un bug | Implementar deduplicación usando msg.id como clave de Redis con TTL de 24h. Verificar antes de procesar. |
| Eventos llegando fuera de orden | Sin garantía de orden — por diseño | Usar campo timestamp para ordenar al construir hilos de conversación. Nunca asumir orden de entrega secuencial. |
| Los reintentos de Meta siguen llegando | Handler devolviendo no-200, timeout (>20s), o crash | Devolver 200 inmediatamente. Mover todo el procesamiento a queue asíncrona. Envolver handler en try-catch. Monitorear estabilidad del servidor. |
| Descarga de medios devuelve 401 | Usando token incorrecto o token expirado | Las descargas de medios requieren el mismo access token usado para la API. Asegurar que es un token de System User permanente, no un token de usuario temporal. Verificar que el token no haya sido revocado. |
Array messages falta en payload |
Es una actualización de estado — el array es statuses, no messages |
Verificar value.messages Y value.statuses por separado. Ambos llegan bajo la suscripción del campo messages. |
| El número del remitente no tiene prefijo + | Cloud API entrega from sin el prefijo + (ej. 15550001234 no +15550001234) |
Normalizar al recibir: '+' + msg.from para obtener formato E.164. SocialHook normaliza esto automáticamente. |
| Timestamp es una cadena, no un entero | Cloud API entrega timestamp como cadena de timestamp Unix |
Siempre parseInt(msg.timestamp, 10) (Node.js) o int(msg["timestamp"]) (Python) antes de operaciones de fecha. |
SocialHook: la capa de webhook gestionada
Todo en esta referencia — verificación HMAC, extracción de payload desde la envelope anidada, parsing de timestamp, normalización del remitente, manejo de reintentos, infraestructura de deduplicación, enrutamiento WABA vs nivel de teléfono — es trabajo de infraestructura en el que tu producto no se diferencia.
SocialHook maneja toda la capa de webhook de Cloud API y entrega un evento normalizado a tu endpoint, para que tu código de aplicación solo vea JSON limpio y consistente — nunca el payload crudo anidado de Meta con sus peculiaridades.
| Aspecto | Cloud API crudo | SocialHook normalizado |
|---|---|---|
| Extracción de mensaje | entry[0].changes[0].value.messages[0] | Objeto message plano en nivel superior |
| Formato del remitente | "15550001234" — sin prefijo + | "+15550001234" — normalizado E.164 |
| Timestamp | "1747231892" — cadena | 1747231892 — entero |
| Verificación HMAC | Tú lo implementas | Hecho — signature_verified: true |
| Reintento en tu downtime | Meta reintenta (7 días) | SocialHook reintenta (3x exponencial) |
| Formato multi-canal | Esquema diferente por plataforma | Mismo esquema: WhatsApp + FB + Instagram |
| Logs de entrega | No disponibles | Log completo por evento |
| Costo mensual | Tus costos de infraestructura de servidor | $50 fijo |
SocialHook cubre WhatsApp, Facebook Messenger y Instagram DMs — los tres canales de mensajería de Meta — bajo una cuenta, una URL de webhook, un formato de payload normalizado. Consulta la referencia completa de payload o comienza con el quickstart de 5 minutos.
Preguntas frecuentes
msg.id en Redis con TTL de 24h usando SET ... NX. Antes de procesar cualquier evento, verifica si el ID existe. Si existe, omítelo. Si no, añádelo y procesa. Esto previene registros duplicados en CRM, respuestas dobles de IA y pagos duplicados.sha256=<hex en minúsculas de 64 caracteres>. El valor hex es HMAC-SHA256 de los bytes brutos del body de la solicitud, usando tu App Secret como clave (encuéntralo en App Settings → Basic — esto es diferente de tu access token). Quita el prefijo sha256= antes de comparar. Usa siempre comparación segura contra timing.messages, las actualizaciones de estado contienen un array statuses. Siempre verifica qué array está presente antes de procesar — intentar leer value.messages[0] en un payload de actualización de estado devuelve undefined.metadata.phone_number_id, y añade webhooks a nivel de número de teléfono solo donde se necesite enrutamiento diferente.Referencia leída.
Ahora recibe el primer webhook.
Conoces la especificación. SocialHook maneja la verificación HMAC, normalización de payload, lógica de reintentos y logging de entrega — para que tu código de aplicación solo vea JSON limpio. Conecta tu número en menos de 5 minutos.