Recibir mensajes reales
💡 🎯 Al terminar esta lección podrás…
- Explicar el apretón de manos de verificación del webhook (hub.mode, hub.verify_token, hub.challenge).
- Exponer tu Worker a Meta mediante un túnel temporal o un despliegue en Cloudflare.
- Registrar la URL y el token de verificación en el panel de Meta y suscribirte al campo messages.
- Leer en los registros un mensaje real y distinguirlo de un evento de estado.
- Diagnosticar por qué un webhook no se verifica o no recibe mensajes.
1.El objetivo: que Meta te hable a ti
Hasta ahora, los mensajes de WhatsApp que tu código procesó eran archivos JSON de ejemplo (test/payloads/*.json). En esta lección ocurre el cambio importante: escribes desde tu celular y el mensaje llega a tu servidor. Para eso hay que hacer tres cosas:
- Que tu servidor sea alcanzable desde internet por HTTPS.
- Registrar esa dirección en Meta y superar la verificación.
- Suscribirte a los eventos que quieres recibir.
mensajePOST (JSON firmado)200 okRequisito: haber hecho la lección 12 (app, número de prueba, destinatario, APP_SECRET, WA_PHONE_ID y un token) y tener el Worker de las lecciones 10 y 11.
⚠️ Importante · Verifica este dato
Los nombres de los menús de Meta (por ejemplo, WhatsApp > Configuration y el botón de editar el webhook) cambian con las actualizaciones del panel. Los pasos de abajo siguen la guía oficial de webhooks; si tu pantalla difiere, busca el equivalente por concepto.
2.Qué exige Meta de tu endpoint
La documentación oficial de webhooks establece estas condiciones:
- La URL debe responder por HTTPS con un certificado válido. Los certificados autofirmados no se aceptan. Por eso
http://localhost:8787no sirve tal cual. - Registras la URL junto con un token de verificación (Verify Token): un texto cualquiera que tú inventas.
- Cada notificación llega por
POSTy debes responder200 OK. - Cada
POSTtrae la cabeceraX-Hub-Signature-256, consha256=seguido de la firma calculada con tu App Secret. Validarla es opcional según la documentación, pero recomendada; el Worker del curso la exige. - Si no respondes bien, Meta reintenta de inmediato y luego varias veces durante 36 horas, cada vez con menos frecuencia. Las notificaciones pueden llegar duplicadas o en lote, así que hay que procesar cada una por separado y deduplicar.
Dos ideas de diseño del Worker de referencia salen directamente de esa lista: la idempotencia (la clave visto:<wamid> que evita responder dos veces al mismo mensaje) y responder 200 al instante y trabajar en segundo plano con ctx.waitUntil.
3.El apretón de manos de verificación
Cuando pulsas «Verificar y guardar» en el panel, Meta envía un GET a tu URL con tres parámetros:
| Parámetro | Qué es |
|---|---|
hub.mode |
Siempre subscribe |
hub.verify_token |
El token que escribiste en el panel |
hub.challenge |
Un número aleatorio que debes devolver |
Tu código debe comprobar que el token coincide y responder 200 con el texto de hub.challenge en el cuerpo. Si no, responder un error. Esto es lo que hace verificarWebhook en src/worker.js:
function verificarWebhook(url, env) {
const modo = url.searchParams.get('hub.mode');
const token = url.searchParams.get('hub.verify_token');
const reto = url.searchParams.get('hub.challenge');
if (modo === 'subscribe' && env.VERIFY_TOKEN && token === env.VERIFY_TOKEN && reto) {
return new Response(reto, { status: 200 });
}
return new Response('Prohibido', { status: 403 });
}
Observa los detalles: se exige que env.VERIFY_TOKEN exista (si lo olvidaste configurar, nadie puede verificar contra un valor vacío) y se devuelve solo el reto, sin comillas ni JSON.
Puedes simular a Meta antes de tocar el panel. Estas son las respuestas reales que dio el Worker en una ejecución con un VERIFY_TOKEN de prueba:
GET /webhook?hub.mode=subscribe&hub.verify_token=token-de-prueba&hub.challenge=1158201444
200 1158201444
GET /webhook?hub.mode=subscribe&hub.verify_token=otro&hub.challenge=1
403 Prohibido
Con curl en tu máquina, cuando el Worker corre en local, sería:
curl "http://localhost:8787/webhook?hub.mode=subscribe&hub.verify_token=TU_VERIFY_TOKEN&hub.challenge=12345"
Si ves 12345, la parte de verificación funciona. Haz esta prueba antes de ir a Meta: así separas los fallos de tu código de los fallos del panel.
4.Opción A: túnel temporal con cloudflared
Para ver mensajes llegando a tu computadora, sin desplegar nada, usa un túnel. Un túnel crea una dirección HTTPS pública que reenvía el tráfico a un puerto local.
- Instala
cloudflareddesde la documentación de Cloudflare. - En una terminal, arranca el Worker en local (necesita
.dev.varscon tus valores de la lección 12):
npx wrangler dev
- En otra terminal, abre el túnel hacia el puerto del Worker (por defecto 8787):
cloudflared tunnel --url http://localhost:8787
cloudflared imprime una dirección temporal https://algo-aleatorio.trycloudflare.com. Según la documentación de Cloudflare, no hace falta cuenta ni dominio, la dirección cambia cada vez que creas un túnel y deja de funcionar al detener el proceso; es solo para pruebas y desarrollo.
⚠️ Importante · Verifica este dato
Estas condiciones de los túneles rápidos (límites, estabilidad) pueden cambiar. Mira la página oficial de Quick Tunnels.
Tu URL de webhook será, por ejemplo, https://algo-aleatorio.trycloudflare.com/webhook. Como cambia en cada sesión, tendrás que actualizarla en Meta cada vez que reinicies el túnel; es la molestia que te empuja a la opción B.
5.Opción B: el Worker desplegado
Si ya desplegaste en la lección 11, tienes una URL estable del tipo https://bot-minimarket.TU-SUBDOMINIO.workers.dev. Es la forma recomendada porque no cambia. Sube los secretos si no lo hiciste:
npx wrangler secret put VERIFY_TOKEN
npx wrangler secret put WA_TOKEN
npx wrangler secret put APP_SECRET
npx wrangler deploy
Prueba que está vivo con curl https://bot-minimarket.TU-SUBDOMINIO.workers.dev/salud; debe responder {"ok":true}. Y para ver los registros en tiempo real mientras llegan mensajes:
npx wrangler tail
📌 Nota · ¿Cuál elegir?
Para aprender y depurar, el túnel con wrangler dev te deja ver todo en tu terminal y editar código al instante. Para dejar el bot funcionando mientras duermes, el despliegue. Muchas personas usan ambos: túnel mientras desarrollan y Worker desplegado como «producción».
6.Registrar el webhook en Meta
En el panel de tu app, sección WhatsApp > Configuration («Configuración») según la guía oficial, busca el bloque Webhook:
- Pulsa Edit («Editar»).
- En Callback URL pega tu URL completa, terminando en
/webhook. - En Verify token pega el mismo texto que pusiste en
VERIFY_TOKEN. - Pulsa Verify and save («Verificar y guardar»).
Si todo va bien, el panel guarda sin errores. Si no, mira los mensajes de error y la tabla de diagnóstico más abajo.
Guardar la URL no basta: además debes suscribirte a los campos que quieres recibir. Para un bot necesitas el campo messages. En el panel verás una tabla de campos del webhook con un interruptor «Subscribe» («Suscribirse») junto a messages. Actívalo. Sin esa suscripción, la verificación es exitosa pero tu servidor jamás recibe un mensaje (un fallo muy frecuente y confuso).
⚠️ Importante · Verifica este dato
Dónde se ven los campos suscritos y cómo se vincula la app a la cuenta de WhatsApp Business (la suscripción de la app a la WABA) ha cambiado entre versiones del panel. Revisa la documentación de webhooks de WhatsApp si, tras suscribirte a messages, siguen sin llegar eventos.
7.La primera prueba real
Con el Worker corriendo y el webhook suscrito:
- Mantén abierta la terminal de
wrangler dev(owrangler tail). - Desde tu celular, escribe
holaal número de prueba (el que aparece en API Setup). - Observa la terminal.
Lo esperable es que Meta haga un POST a /webhook con un JSON como el de test/payloads/texto.json (con datos de ejemplo, no reales):
{
"object": "whatsapp_business_account",
"entry": [{
"id": "102290129340398",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": { "display_phone_number": "51999000222", "phone_number_id": "106540352242922" },
"contacts": [{ "profile": { "name": "Cliente Ejemplo" }, "wa_id": "51999000111" }],
"messages": [{ "from": "51999000111", "id": "wamid.TEXTO001", "timestamp": "1791360000", "text": { "body": "Quiero 2 leches" }, "type": "text" }]
},
"field": "messages"
}]
}]
}
Tu Worker comprueba la firma, responde 200 ok y procesa el mensaje en segundo plano. Con una firma correcta y con una incorrecta, las respuestas reales fueron:
POST /webhook (firma válida) -> 200 ok
POST /webhook (firma falsa) -> 401 Firma inválida
Y como en esa prueba el token de WhatsApp era falso, al intentar contestar el Worker registró el error que devuelve Meta cuando el token no es válido:
WhatsApp respondió 401: Invalid OAuth access token - Cannot parse access token
Es una buena señal de diagnóstico: significa que el mensaje sí llegó y se procesó, y que el fallo está en el envío (token). Con un token real te contestará el bot; el envío es el tema de la lección 14.
8.Mensajes y estados: dos clases de evento
Tu webhook no solo recibe mensajes de clientes. Cuando el bot envía algo, Meta te avisa del destino de cada mensaje con un evento de estado (sent, delivered, read, failed). Llegan por el mismo campo messages, pero en value.statuses en vez de value.messages. La función parsearWebhook de src/whatsapp.js los separa:
for (const msg of valor.messages ?? []) {
const evento = parsearMensaje(msg, valor.contacts ?? [], phoneNumberId);
if (evento) eventos.push(evento);
}
for (const st of valor.statuses ?? []) {
if (!st?.id) continue;
eventos.push({ tipo: 'estado', id: st.id, estado: st.status, /* ... */ });
}
Por eso, al probar, verás más eventos de los que esperabas: tu mensaje entrante y, después, los estados del mensaje que respondió el bot. No es un error. También aprenderás a ignorar con elegancia lo que no entiendes: imágenes, audios y otros tipos llegan como no_soportado y el bot responde con un texto amable en lugar de romperse.
9.Cuando no llega nada: diagnóstico
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| El panel no verifica la URL | Token distinto, URL sin /webhook, o Worker sin VERIFY_TOKEN |
Prueba con curl el GET de verificación (arriba) |
| Verifica, pero no llegan mensajes | Falta suscribirse al campo messages |
Activa la suscripción en el panel |
| Llegan, pero el Worker responde 401 | APP_SECRET equivocado o el cuerpo se modificó antes de verificar la firma |
Revisa el secreto; usa request.text() antes de JSON.parse |
| Te llega el mismo mensaje varias veces | Tu servidor tardó o falló y Meta reintentó | Responde 200 rápido; mantén la idempotencia (visto:) |
| Con túnel dejó de funcionar | La URL temporal cambió | Reinicia el túnel y actualiza la URL en Meta |
| Escribes desde un número que no registraste | El número de prueba solo trabaja con destinatarios registrados | Registra ese número en el panel |
⚠️ Importante · Verifica este dato
Si Meta no logra entregar a tu URL durante mucho tiempo, descarta las notificaciones (según la documentación, tras 36 horas). Un bot caído un fin de semana puede perder mensajes; en producción conviene monitorear (lección 23).
10.Errores frecuentes
- Poner la URL de
localhosten el panel. Meta no puede alcanzar tu computadora: necesita una URL pública HTTPS (túnel o despliegue). - Olvidar el
/webhookal final de la URL. El Worker acepta elGETde verificación en/webhook; con la raíz también funciona en el código del curso, pero lo correcto es ser explícito. - Verificar sin suscribirse a
messages. La URL queda «verificada» y aun así no llega nada. - Parsear el JSON antes de comprobar la firma. La firma se calcula sobre el texto exacto; si lo reformateas, deja de coincidir.
- Tardar en responder. Meta reintenta si no recibe
200; responde primero y trabaja después. - Pegar el token real en la terminal compartida o en capturas. En las pruebas usa siempre valores como
token-de-pruebahasta que sea imprescindible.
11.Apuntes para llevar
- Meta exige una URL HTTPS pública con certificado válido; localhost solo sirve con un túnel.
- La verificación es un
GETconhub.mode,hub.verify_tokenyhub.challenge: devuelves el reto si el token coincide. - Hay que suscribirse al campo
messages; la verificación sola no basta. - Los mensajes llegan por
POSTfirmado (X-Hub-Signature-256); responde200rápido y procesa en segundo plano. - Meta reintenta durante 36 horas y puede duplicar o agrupar: procesa cada evento por separado y deduplica.
- Los eventos de estado llegan por el mismo webhook que los mensajes.
12.Glosario
| Término | Significado |
|---|---|
| Callback URL | Dirección HTTPS de tu servidor donde Meta entrega los eventos. |
| Verify Token | Texto que tú inventas y que Meta devuelve en la verificación para probar que la URL es tuya. |
| hub.challenge | Número aleatorio que tu endpoint debe devolver tal cual durante la verificación. |
| Suscripción a campos | Elección de qué tipos de evento (por ejemplo messages) quieres recibir. |
| Túnel | Servicio que expone un puerto local mediante una URL pública HTTPS. |
| Idempotencia | Propiedad de procesar igual un evento aunque llegue repetido. |
| Evento de estado | Aviso de Meta sobre el destino de un mensaje enviado: sent, delivered, read o failed. |
🧠 Comprueba lo aprendido
Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.
Meta envía un GET con hub.mode=subscribe, hub.verify_token y hub.challenge. ¿Qué debe responder tu endpoint?
Si el token coincide devuelves el reto tal cual; si no, un error como 403.
Verificaste la URL en el panel pero no llega ningún mensaje. ¿Qué revisas primero?
La verificación y la suscripción son pasos distintos; sin suscribirte a messages no hay entregas.
¿Por qué no basta con http://localhost:8787 como Callback URL?
Un túnel o un despliegue en Cloudflare resuelve esa exigencia.
¿Qué debe hacer tu servidor ante una notificación de Meta?
Si no respondes 200, Meta reintenta durante horas y puede duplicar eventos.
Recibes un evento con value.statuses y status "delivered". ¿Qué es?
Los estados (sent, delivered, read, failed) llegan por el mismo webhook, en statuses.
📚 Para profundizar (fuentes oficiales)
Este material lo hace un estudiante con ayuda de IA: ante cualquier duda, manda la documentación oficial.
Tu progreso se guarda en este navegador y se refleja en el índice del curso.
💡 🚀 ¡Ahora te toca!
La práctica fija lo aprendido. Pasa al ejercicio de esta lección.