InstaTunnel
Abrir menu de navegacion
Tutorial
11 min read
29 views

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.

IT
InstaTunnel Team
Published by the InstaTunnel team | Editorial policy
WhatsApp Cloud API Webhooks en localhost: El apretón de verificación de token, reintentos y configuración del 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 GET que contiene hub.mode, hub.verify_token y hub.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 GET cada 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 a POST (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 GET handshake. 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 (como message_template_status_update) y webhooks a nivel de cuenta (como account_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

  1. Prueba el handshake manualmente. Solicita tu URL con ?hub.mode=subscribe\u0026hub.verify_token=YOUR_TOKEN\u0026hub.challenge=123 y confirma que 123 regresa como texto plano con estado 200.
  2. Compara tokens carácter por carácter, incluyendo espacios finales.
  3. Confirma la suscripción a messages, luego verifica GET /{WABA_ID}/subscribed_apps.
  4. Verifica el modo de la app. Algunos webhooks no se envían mientras la app está en modo Dev.
  5. Busca una anulación que oculte tu URL a nivel de la app.
  6. Verifica el túnel. Los reintentos hacen que un túnel caído parezca un problema de Meta.
  7. 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

Continue from this article into the most relevant product guides and workflows.

Related Topics

#whatsapp cloud api webhook#whatsapp cloud api webhooks localhost#whatsapp webhook verify token#hub.challenge#hub.verify_token#whatsapp webhook verification#meta webhook verification#whatsapp webhook handshake#X-Hub-Signature-256#whatsapp webhook signature validation#verify webhook signature#meta app secret#whatsapp app secret#whatsapp webhook callback url#whatsapp webhook localhost#test whatsapp webhook locally#whatsapp webhook local development#localhost tunnel#localhost tunneling#expose localhost to internet#ngrok alternative#stable subdomain tunnel#static tunnel subdomain#persistent callback url#webhook testing localhost#webhook debugging#webhook retries#meta webhook retries#whatsapp webhook 200 response#whatsapp webhook not working#whatsapp webhook verification failed#whatsapp webhook GET request#whatsapp webhook POST request#whatsapp business api#whatsapp business platform#meta for developers#meta developer app#facebook developers webhook#whatsapp api tutorial#whatsapp api node.js#whatsapp webhook express#whatsapp webhook python flask#whatsapp webhook fastapi#whatsapp chatbot development#whatsapp bot webhook#whatsapp message webhook#whatsapp incoming messages webhook#HMAC SHA256 webhook#webhook security#webhook authentication#webhook signature verification#developer tools#devops#api integration#cloud native#backend development#webhook tutorial#instatunnel

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles