WhatsApp Cloud API Webhooks en localhost: El apretón de verificación de token, reintentos y configuración del secreto de la app
Configura webhooks de WhatsApp Cloud API en localhost. Gestiona el handshake hub.challenge de verificación de token, las comprobaciones de X-Hub-Signature-256, los reintentos y el secreto de la app.

Quick answer
Webhooks de WhatsApp Cloud API en localhost: Configuración del token de verificación: webhook testing answer
For local webhook testing, run your app locally, expose it with a public HTTPS tunnel, and paste the stable callback URL into the provider dashboard.
How do I test webhooks on localhost?
Start your local server, open a public HTTPS tunnel to that port, configure the provider webhook URL, and inspect events in your local logs.
Why does a stable webhook URL matter?
Stable URLs prevent provider dashboards from needing manual callback updates every time you restart a tunnel.
Empiezas tu bot de WhatsApp en localhost:3000, abres un túnel, pegas la URL pública en el Meta App Dashboard, haces clic en Verificar y guardar, y obtienes un error rojo. O funciona una vez; reinicias el túnel a la mañana siguiente, y el panel vuelve a pedir verificar.
Ambos problemas comparten una causa: la onboarding de webhooks de Meta es un protocolo de verificación (handshake), no un simple interruptor de “envíame eventos”. Meta envía primero un GET con un valor hub.challenge. Solo después de responder correctamente, empieza a entregar peticiones POST firmadas. Si no reconoces esas respuestas con un 200, Meta vuelve a intentarlo durante hasta 7 días en la WhatsApp Business Platform.
Esta guía cubre el handshake, el comportamiento de reintentos y la configuración del secreto de la app, y luego muestra por qué un subdominio estable transforma una tarea recurrente en un registro de una sola vez.
Resumen
- Meta verifica tu URL de callback con un
GETque contienehub.mode,hub.verify_tokenyhub.challenge. Devuelve HTTP 200 con el valor del challenge, pero solo si el token coincide. - El token de verificación es una cadena que eliges. El secreto de la app es emitido por Meta. Son credenciales distintas.
- La verificación por sí sola no entrega nada. Debes suscribirte también al campo
messages. - WhatsApp reintenta entregas fallidas durante hasta 7 días, así que espera duplicados y eventos obsoletos.
- Meta envía una nueva verificación
GETcada vez que configuras o editas la URL de callback o el token de verificación. Una URL de túnel que cambia, por tanto, significa repetir el handshake. Un subdominio estable significa registrarte una sola vez.
Por qué Meta no puede llamar localhost
Los servidores de Meta están en Internet público; tu portátil está detrás de NAT sin puertos entrantes abiertos. Meta también requiere un certificado TLS válido en tu endpoint, y su documentación indica que los certificados autofirmados no están soportados.
Entonces una integración local necesita tres cosas:
- Un servidor que responda tanto a
GET(verificación) como aPOST(eventos) en una misma ruta. - Una URL pública HTTPS que redirija el tráfico hacia él.
- Como el panel almacena una URL de callback por app, una URL que permanezca igual entre sesiones.
Paso 1: El handshake de verificación de token
En el Meta App Dashboard, abre WhatsApp → Configuration. Si creaste tu app con el caso de uso “Conectar con clientes a través de WhatsApp”, el panel se encuentra bajo Use cases → Customize → Configuration en su lugar. Ingresas una URL de callback y un token de verificación. El token es cualquier cadena que eliges; Meta no lo emite. Meta envía una solicitud de verificación cada vez que configuras o editas cualquiera de los dos campos:
GET /webhook?hub.mode=subscribe\u0026hub.verify_token=YOUR_TOKEN\u0026hub.challenge=1158201444
| Parámetro | Significado |
|---|---|
hub.mode |
Siempre subscribe durante la verificación |
hub.verify_token |
La cadena que escribiste en el panel |
hub.challenge |
Un valor aleatorio generado por Meta |
La documentación de Meta establece la condición de aprobación: responder con el estado 200 y el valor hub.challenge. Si respondes algo distinto, Meta considera el endpoint como no verificado y no envía webhooks.
app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === process.env.VERIFY_TOKEN) {
// Raw challenge, plain text, no JSON wrapper
return res.status(200).type('text/plain').send(challenge);
}
return res.sendStatus(403);
});
En FastAPI no puedes nombrar un parámetro de Python como hub.mode, así que usa Query(alias="hub.mode"). PHP tiene una curiosidad relacionada: convierte puntos en los nombres de parámetros de consulta a guiones bajos, así que allí lee hub_mode, hub_verify_token y hub_challenge.
Errores comunes del handshake
| Síntoma | Causa probable | Solución |
|---|---|---|
| “Callback URL or verify token couldn’t be validated” | Respuesta JSON, estado no-200, o un desajuste de token de un solo carácter | Devuelve el challenge crudo como texto plano con 200 |
| El navegador muestra un error de parámetro faltante | Abriste la URL sin la cadena de consulta de Meta | Esperado; solo GET de Meta transporta hub.challenge |
| Timeout durante la verificación | Servidor o túnel aún no está en ejecución | Inicia el servidor, luego el túnel, luego haz clic en verificar |
| Handshake pasa, pero no llega nada | No hay campo webhook suscrito | Suscríbete a messages |
La fila de timeouts es específica de localhost. Meta llama a tu URL en vivo cuando guardas, así que tu servidor y el túnel deben estar activos.
Verificación de token vs. secreto de la app
La gente los confunde constantemente, y Meta los usa para trabajos distintos.
- El token de verificación es elegido por ti y se usa en el
GEThandshake. La guía de WhatsApp de Hookdeck señala que solo se usa una vez, durante la configuración. - El secreto de la app es emitido por Meta, se encuentra en App settings → Basic, y es la clave para firmar cada entrega de eventos.
Manténlos en variables de entorno separadas, por ejemplo VERIFY_TOKEN y META_APP_SECRET. Si usas el token de verificación donde corresponde el secreto de la app, cada mensaje real fallará la validación.
Cada POST lleva un encabezado X-Hub-Signature-256 en la forma sha256=<hex>, un HMAC-SHA256 del payload con la clave de tu app secret. Meta dice que validarlo es opcional pero recomendado. Calcula el HMAC sobre los bytes crudos de la solicitud, antes de que cualquier middleware JSON toque el cuerpo. Hookdeck señala que Meta firma una versión con caracteres Unicode escapados del payload, así que un cuerpo que parseaste y re-serializaste no coincide.
const crypto = require('crypto');
// express.raw() mantiene los bytes exactos que firmó Meta
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.META_APP_SECRET)
.update(req.body)
.digest('hex');
const received = req.get('x-hub-signature-256') || '';
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
if (!valid) return res.sendStatus(401);
res.sendStatus(200); // acknowledgement first
queue.push(JSON.parse(req.body)); // processing asíncrono
});
La firma no lleva timestamp. Hookdeck’s Facebook webhooks guide apunta que un payload debidamente firmado también validaría si se reprodujera, así que deduplica por IDs de mensaje en lugar de confiar en la firma como prueba de frescura.
Paso 2: Suscríbete al campo messages
La verificación prueba que la URL es tuya; no selecciona tus eventos. Después de que tiene éxito, el dashboard muestra una lista de campos de webhook. Para un chatbot o bandeja de entrada, suscríbete a messages, que transporta mensajes entrantes y el estado de los mensajes que envías (enviado, entregado, leído, fallido).
La documentación de Meta enumera los permisos: whatsapp_business_messaging para webhooks de messages y whatsapp_business_management para todos los demás webhooks.
Si el panel recibe eventos de prueba pero los mensajes reales no, verifica que tu app esté suscrita a la WhatsApp Business Account (WABA) en sí. Hookdeck y Webhook Relay reportan este modo de fallo: la URL está verificada, pero la app nunca estuvo suscrita a la WABA. La solución es POST /{WABA_ID}/subscribed_apps, y GET en el mismo endpoint lo confirma. También confirma que tu app esté en modo Live, porque Meta señala que algunos webhooks no se envían a apps en modo Dev.
Paso 3: Reintentos, duplicados y la ventana de 7 días
Meta trata cualquier cosa que no sea HTTP 200 como una entrega fallida. La ventana de reintentos depende del producto:
- WhatsApp Business Platform: La documentación de Meta dice que la entrega se reintenta de inmediato y luego con menor frecuencia durante hasta 7 días, después de lo cual los webhooks no reconocidos se descartan.
- Webhooks de Graph API genéricos: La página de “Get started with webhooks” de Meta describe reintentos durante 36 horas, después de lo cual las actualizaciones no reconocidas se descartan.
Estas describen productos diferentes, así que no se contradicen. Para WhatsApp, planea alrededor de 7 días. Una excepción: para números que usan la configuración de “No Storage” de Meta, los reintentos de webhooks están limitados a 1 hora, y un webhook entrante no entregable se descarta con el código de error 131035.
Meta también advierte que los reintentos se envían a todas las apps suscritas a los webhooks de la WABA, lo que puede producir notificaciones duplicadas. Eso tiene cuatro consecuencias.
Reconoce primero, procesa luego. Devuelve 200, y luego realiza el trabajo lento (escrituras en base de datos, llamadas a LLM, respuestas) desde una cola. Meta no publica un único timeout universal, y la documentación BSP varía. 360dialog, por ejemplo, documenta un límite estricto de cinco segundos para un 200, así que mantén tu manejador rápido.
Deduplicar. Vincula los mensajes entrantes por el ID de mensaje (wamid...) y los eventos de estado por el ID de estado más el valor de estado, ya que un mensaje saliente legítimamente genera sent, delivered y read.
No confíes en el orden de llegada. Hookdeck señala que WhatsApp no garantiza el orden, de modo que un estado read puede llegar antes que delivered. Usa el campo timestamp dentro de cada evento en lugar de tu hora de recepción.
Espera eventos obsoletos tras una caída local. Si cierras el portátil de noche y el túnel se cae, Meta sigue reintentando la URL registrada, y puede llegar una avalancha de eventos antiguos cuando te reconectes. Aumenta también tu límite de procesamiento. Meta dice que las cargas útiles de webhook pueden ser de hasta 3 MB, y un 413 de tu servidor cuenta como una entrega fallida que Meta reintenta.
Meta no ofrece una API para obtener datos históricos de webhooks, así que registra las cargas útiles crudas al recibirlas si las necesitas más tarde.
Por qué un subdominio estable cambia el flujo de trabajo
Meta almacena una URL de callback por app, y editarla repite el handshake. Si tu túnel reparte un nombre de host aleatorio en cada inicio, cada mañana se vuelve: iniciar el servidor, iniciar el túnel, copiar la URL, pegarla en el panel, hacer clic en verificar mientras tu servidor está despierto, y esperar que el token no tenga error. Hookdeck señala lo mismo sobre túneles en su guía de WhatsApp: la URL cambia en cada reinicio a menos que pagues por un subdominio fijo.
Un subdominio estable reduce eso a un registro único. Verifica https://yourname.example-tunnel.dev/webhook una vez, y podrás reiniciar el túnel, reiniciar, o cambiar de portátil sin tocar la configuración de Meta, siempre y cuando el mismo nombre apunte a tu máquina. Los servicios de túnel con subdominios reservados, InstaTunnel entre ellos, existen por esta razón. Verifica que el nombre persista entre reinicios, no solo durante las reconexiones en una misma sesión.
También ayuda con el escenario de reintentos anterior: los eventos reintentos van a la dirección registrada, por lo que un hostname estable permite que una reconexión retome desde donde dejó Meta.
Mantén dev y prod separados con anulaciones
Si una misma app de Meta atiende a ambos entornos, no cambies la URL a nivel de la app cuando cambies de entorno. Meta soporta URLs de callback alternas por WABA o por número de teléfono comercial. Cuando se dispara un campo soportado, Meta verifica primero el número de teléfono, luego la WABA, y finalmente recurre a la URL de callback de la app.
curl -X POST "https://graph.facebook.com/$GRAPH_API_VERSION/$TEST_WABA_ID/subscribed_apps" \\
-H "Authorization: Bearer $ACCESS_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"override_callback_uri": "https://dev.example-tunnel.dev/webhook",
"verify_token": "'"$DEV_VERIFY_TOKEN"'"
}'
Para un solo número, envía un objeto webhook_configuration con los mismos dos campos a POST /{PHONE_NUMBER_ID}. Según los documentos de anulación de Meta:
- La URL está limitada a 200 caracteres, y tanto la URL como el token de verificación son obligatorios.
- Las anulaciones se aplican solo a ciertos campos, incluido
messages. Webhooks de plantillas (comomessage_template_status_update) y webhooks a nivel de cuenta (comoaccount_update) siempre van a la URL de la app. - Puedes inspeccionar la configuración activa con
GET /{PHONE_NUMBER_ID}?fields=webhook_configuration, que muestra el teléfono, la WABA y las URLs de la aplicación. - Suscribir la app a la WABA de nuevo sin parámetros del cuerpo elimina una anulación a nivel de WABA.
Usa la versión de Graph API que tu app apunta en lugar de $GRAPH_API_VERSION. Un hostname de tipo dev. permanente es un objetivo de anulación idóneo para tu número de prueba, mientras que la producción mantiene su propia URL.
Nota sobre el cambio mTLS de 2026
Si habilitaste TLS mutuo en tu suscripción de webhook, Meta anunció que a partir del 31 de marzo de 2026 su certificado de cliente sería firmado por una nueva CA propiedad de Meta. El nombre común se mantiene como client.webhooks.fbclientcerts.com. El anuncio de Graph API v25 de Meta explicó que el certificado firmado por DigiCert no podía renovarse porque DigiCert está depreciando EKU de Client Authentication. La raíz de ese certificado expiró el 15 de abril de 2026. Se dijo a los desarrolladores que añadieran meta-outbound-api-ca-2025-12.pem a su trust store antes de la fecha límite. Un servidor que exigiera mTLS y omitiera esa actualización dejaría de recibir webhooks por completo, y la solución es un cambio en el trust store.
Meta también afirma que mTLS está habilitado por aplicación, no por cuenta de mensajería o número de teléfono. La mayoría de las configuraciones de localhost no usan mTLS. Si deseas restringir el tráfico entrante, Meta sugiere usar mTLS en lugar de listar sus rangos de IP, ya que esas direcciones cambian periódicamente.
Resolución de problemas: no se reciben mensajes por webhook
- Prueba el handshake manualmente. Solicita tu URL con
?hub.mode=subscribe\u0026hub.verify_token=YOUR_TOKEN\u0026hub.challenge=123y confirma que123regresa como texto plano con estado 200. - Compara tokens carácter por carácter, incluyendo espacios finales.
- Confirma la suscripción a
messages, luego verificaGET /{WABA_ID}/subscribed_apps. - Verifica el modo de la app. Algunos webhooks no se envían mientras la app está en modo Dev.
- Busca una anulación que oculte tu URL a nivel de la app.
- Verifica el túnel. Los reintentos hacen que un túnel caído parezca un problema de Meta.
- Verifica que la validación de la firma no esté rechazando POST válidos por un secreto incorrecto o un cuerpo re-serializado.
Preguntas frecuentes
¿Qué es el token de verificación de webhooks de WhatsApp?
Una cadena que eliges e introduces en el Meta App Dashboard. Meta la devuelve como hub.verify_token, y tu servidor la compara con el valor almacenado antes de devolver hub.challenge.
¿Qué debería devolver el endpoint de verificación?
HTTP 200 con el valor hub.challenge en el cuerpo de la respuesta. Devolverlo como texto plano, sin envoltorio JSON, es la opción segura. Cualquier otra respuesta deja el endpoint sin verificar.
¿Cuánto tiempo reintenta Meta los webhooks de WhatsApp fallidos?
La documentación de WhatsApp de Meta dice que hasta 7 días, con menor frecuencia (1 hora para números No Storage). Los webhooks de Graph API genéricos usan una ventana más corta de 36 horas.
¿Necesito volver a verificar si cambio mi puerto local?
No, siempre que la URL pública y la ruta permanezcan igual, porque Meta solo conoce la dirección pública. Meta re-verifica cada vez que configuras o editas la URL de callback o el token de verificación, así que un nuevo hostname o ruta significa un nuevo handshake.
Conclusión
El modelo de Meta tiene tres partes: un handshake GET asegurado por un token de verificación que tú inventaste, entrega POST firmadas con el app secret emitido por Meta, y una red de reintentos de 7 días que premia reconocimientos rápidos y controladores idempotentes. El desarrollo en localhost es problemático solo cuando la URL pública sigue cambiando. Soluciónalo con un subdominio estable: regístralo una vez, suscríbete a messages, y mantén separados el token de verificación y el app secret.
Fuentes
- Crear un endpoint de webhook, WhatsApp Business Platform (Meta)
- Vista general de webhooks, WhatsApp Business Platform (Meta)
- Anulaciones de webhooks (Meta)
- Comienzo con webhooks, Graph API (Meta)
- WhatsApp Business Platform, No Storage (Meta)
- Presentando Graph API v25 (Meta, Feb 2026)
- Webhooks de WhatsApp Cloud API: Configuración, Token de verificación y Payloads (Webhook Relay)
- Guía de Webhooks de WhatsApp (Hookdeck)
- Guía de Facebook Webhooks (Hookdeck)
- Recibir mensajes vía Webhook (360dialog)
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.