Webhooks de WhatsApp Business API: tutorial paso a paso 2026
Configura webhooks de WhatsApp Business API: endpoint HTTPS, handshake Meta, validación HMAC e idempotencia. Con código en Node.js y Python.
El 80% de los agentes de WhatsApp que diagnostico en LATAM no están fallando por el modelo, ni por el prompt, ni por la base de conocimiento. Están fallando porque el webhook está mal configurado. A veces devuelve 500. A veces la firma no valida. A veces el endpoint es HTTP en lugar de HTTPS. Y el síntoma siempre es el mismo: el bot no responde, y nadie sabe por qué.
En este tutorial te llevo de cero a un webhook de WhatsApp Business API funcionando en producción. Vamos a montar el endpoint, pasar el handshake de Meta, validar la firma HMAC-SHA256 para que nadie te inyecte eventos falsos, suscribirnos a los eventos correctos, y dejar el sistema idempotente para que el mismo evento no se procese dos veces. El código está en Node.js y Python, los dos stacks que cubren al 90% de los equipos que me escriben desde México, Colombia, Argentina y Chile.
Si tu agente de WhatsApp “anda a veces y a veces no”, lo que sigue te va a ahorrar horas.
TL;DR
- Un webhook de WhatsApp es un endpoint HTTPS público donde Meta te entrega cada evento de tu número (mensajes, estados, clics) en tiempo real. Sin él, tu agente no recibe nada.
- Necesitás HTTPS válido, una URL accesible desde Internet, y un App Secret para validar que cada POST viene realmente de Meta.
- El handshake inicial es un GET con
hub.mode=subscribe,hub.verify_tokenyhub.challenge. Devolvés el challenge y Meta te deja pasar. - Después validás cada POST con
X-Hub-Signature-256(HMAC-SHA256 sobre el cuerpo crudo) en comparación de tiempo constante. Sin esto, tu endpoint es un colador. - Meta rotó su CA el 31 de marzo de 2026. Si tu trust store no se actualizó, dejás de recibir eventos sin error visible.
Por qué los webhooks son el corazón de todo lo que hacés en WhatsApp
Acá va una verdad que pocos dicen en voz alta: WhatsApp Cloud API no funciona con polling. No podés hacer GET /messages cada 30 segundos a ver si llegó algo nuevo. Todo el modelo es push. Cuando un cliente te escribe, Meta le avisa a tu servidor mediante un POST a la URL que vos registraste. Si esa URL no existe, no responde 200, o está caída, ese mensaje se pierde. No hay backup. No hay retry infinito.
En mi experiencia, los tres lugares donde un agente LATAM se rompe son: el webhook devuelve 500 y Meta lo desactiva, el operador cambió la URL del endpoint y olvidó actualizar Meta, o el certificado TLS venció y nadie se enteró. Los tres se diagnostican en menos de 5 minutos si sabés dónde mirar.
Y los webhooks no son solo para mensajes entrantes: Meta también te avisa cuando una plantilla se aprueba, cuando se entrega un mensaje, cuando el cliente lo lee, cuando hace clic en un botón, cuando tu número pierde calidad, o cuando Meta te banea. Son 18 campos disponibles. Suscribirse a los correctos te da visibilidad operativa sin entrar al dashboard cada hora.
Qué necesitás antes de empezar
Cinco elementos antes de tocar código:
- Número de WhatsApp Business API activo, ya conectado a tu WABA.
- App en Meta for Developers con el producto WhatsApp agregado (te da un App ID y un App Secret).
- Phone Number ID y WABA ID, que aparecen en WhatsApp Manager > Phone Numbers.
- Endpoint HTTPS público con certificado válido (VPS, Lambda, Cloudflare Worker, etc). En dev,
ngrokocloudflaredpara exponer localhost. - Stack de tu preferencia. Acá voy con Node.js (Express) y Python (FastAPI). Si usás otro, la lógica es la misma.
El App Secret está en App Settings > Basic. Guardalo como variable de entorno, nunca lo commitees.
Paso 1: Diseñá tu endpoint receptor
Tu endpoint maneja dos verbos HTTP en la misma ruta. GET para el handshake inicial (Meta confirma que la URL te pertenece). POST para los eventos (cada vez que algo pasa en tu número, Meta te manda un JSON).
La regla de oro: el POST tiene que devolver 200 OK en menos de 5 segundos. Si tardás más, Meta reintenta. Si fallás varias veces, Meta desactiva la suscripción. Por eso, en producción validás la firma, encolás el evento (Redis, SQS, BullMQ), y devolvés 200 al toque. El procesamiento pesado lo hace un worker aparte.
En Node.js con Express
import express from 'express';
import crypto from 'crypto';
const app = express();
// IMPORTANTE: capturar el raw body para validar la firma.
// express.json() no debe correr antes que este middleware.
app.use('/webhook', express.json({
verify: (req, _res, buf) => {
req.rawBody = buf;
}
}));
app.get('/webhook', (req, res) => {
const VERIFY_TOKEN = process.env.VERIFY_TOKEN;
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
return res.sendStatus(403);
});
app.post('/webhook', (req, res) => {
if (!isValidSignature(req)) {
return res.sendStatus(401);
}
// Responder 200 rápido y procesar async
res.sendStatus(200);
processEventAsync(req.body).catch(console.error);
});
function isValidSignature(req) {
const header = req.get('x-hub-signature-256');
if (!header) return false;
const expected =
'sha256=' +
crypto
.createHmac('sha256', process.env.APP_SECRET)
.update(req.rawBody)
.digest('hex');
const a = Buffer.from(header);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.listen(3000);
En Python con FastAPI
import os, hmac, hashlib
from fastapi import FastAPI, Request, Response
app = FastAPI()
def verify_signature(raw_body: bytes, header: str) -> bool:
if not header or not header.startswith("sha256="):
return False
expected = "sha256=" + hmac.new(
os.environ["APP_SECRET"].encode("utf-8"),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(header, expected)
@app.get("/webhook")
async def handshake(request: Request):
params = request.query_params
if (params.get("hub.mode") == "subscribe"
and params.get("hub.verify_token") == os.environ["VERIFY_TOKEN"]):
return Response(content=params.get("hub.challenge", ""),
media_type="text/plain")
return Response(status_code=403)
@app.post("/webhook")
async def receive(request: Request):
raw = await request.body()
sig = request.headers.get("x-hub-signature-256", "")
if not verify_signature(raw, sig):
return Response(status_code=401)
enqueue(raw)
return Response(status_code=200)
Fijate en el detalle del verify en Express y el await request.body() en FastAPI. Ambos capturan los bytes crudos antes de que el framework los parsee. Esto es crítico: si parseás JSON y re-serializás, el orden de las claves puede cambiar, y la firma ya no va a coincidir. Es uno de los bugs más comunes que veo en producción.
Paso 2: Registralo en Meta Business Manager
Con tu endpoint accesible (aunque sea vía ngrok), andá al dashboard:
- Entrá a developers.facebook.com y seleccioná tu app.
- WhatsApp > Configuration > Webhook > Edit.
- En Callback URL, pegá tu URL:
https://tu-dominio.com/webhook. - En Verify Token, escribí un string aleatorio que vas a guardar en tu variable de entorno.
- Click en Verify and Save. Meta hace un GET a tu endpoint con
hub.mode=subscribe&hub.challenge=...&hub.verify_token=TU_TOKEN. Si devolvés el challenge correcto, la suscripción queda activa.
Si te aparece “Could not verify”, casi siempre es una de tres: (a) token no coincide, (b) devolviste algo distinto al challenge, o (c) tu URL no responde por firewall. Mirá los logs de tu servidor.
Paso 3: Suscribite a los campos correctos
Para un agente LATAM promedio, suscribite a cinco campos:
| Campo | Para qué sirve |
|---|---|
messages | Esencial. Mensajes entrantes y estados de salida. Sin este, no hay bot. |
message_template_status_update | Cuando Meta aprueba, rechaza o marca como spam una plantilla. |
message_template_quality_update | Cambios en calidad de plantillas (HIGH/MEDIUM/LOW). |
phone_number_quality_update | Cambios en calidad de tu número (GREEN/YELLOW/RED). |
account_update | WABA: verificaciones, política, límites de messaging. |
No te suscribas a todo “por si acaso”. Cada campo activado es un POST extra. Con cinco cubrís el 95% de la operación.
Paso 4: Validá la firma HMAC-SHA256
Esto es lo que separa un webhook seguro de uno que es un colador. Cada POST de Meta viene con un header X-Hub-Signature-256 que es un HMAC-SHA256 del cuerpo crudo de la request, usando tu App Secret como clave. Si no validás, cualquiera que descubra tu URL puede mandarte POSTs con JSON inventado y tu bot va a responder a clientes que nunca escribieron. En producción LATAM ya vi casos así.
La comparación debe hacerse en tiempo constante: crypto.timingSafeEqual en Node y hmac.compare_digest en Python. Un === corta apenas encuentra diferencia, y eso filtra información al atacante sobre cuántos bytes de la firma adivinó.
// MAL — comparación con === (vulnerable a timing attack)
if (req.get('x-hub-signature-256') === expected) { ... }
// BIEN — comparación de tiempo constante
const a = Buffer.from(header);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
La diferencia es una línea y muchos agentes en producción la tienen mal.
Paso 5: Implementá idempotencia desde el día uno
Meta reintenta. No es un “si pasa”, es un “va a pasar”. Si tu endpoint devuelve 500, 429, o tarda más de 5 segundos, Meta vuelve a mandar el mismo POST. A veces 2 veces, a veces 10. Lo he visto en picos de campaña de Black Friday.
Cada mensaje entrante tiene un ID único en messages[].id (formato wamid.HBgN...). Si tu worker procesa ese ID dos veces, terminás enviando dos respuestas al mismo cliente, dos entradas al CRM, dos tickets abiertos. La solución: antes de procesar, fijate si ese wamid ya está en tu store con TTL de 24-48 horas. Redis funciona perfecto.
async function processEvent(payload) {
const messageId = payload.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.id;
if (!messageId) return;
const already = await redis.get(`msg:${messageId}`);
if (already) return;
await redis.setex(`msg:${messageId}`, 86400, '1'); // 24h TTL
await handleMessage(payload);
}
En mi experiencia, dedicar 15 minutos a esto al inicio ahorra horas de debugging cuando alguien te reporta “el bot me respondió dos veces”.
Paso 6: Devolvé 200 en milisegundos
El endpoint que recibe el POST de Meta tiene que ser una “thin surface”: hace lo mínimo indispensable (verificar firma, encolar, devolver 200) y nada más. La lógica pesada (llamar al LLM, consultar la base de datos, escribir en el CRM) va en un worker aparte que consume la cola. Si tu LLM tarda 4 segundos, ya estás al límite de los 5 que Meta espera. Si tarda 6, Meta lo ve como timeout, reintenta, y en una hora de tráfico normal podés terminar con cientos de reintentos duplicados. Separar webhook y worker te permite escalar cada pieza de forma independiente.
Errores comunes en LATAM
Estos son los errores que más se repiten, en orden de frecuencia.
Error 1 — HTTP en lugar de HTTPS. Meta no acepta URL http://. Ni en dev, ni en staging, ni en producción. Si estás en local, usá ngrok, cloudflared, o un certificado autofirmado en un dominio real. Let’s Encrypt te da certificados gratis en 5 minutos; no hay excusa.
Error 2 — Validar firma sobre el JSON re-serializado. Si usás express.json() sin el verify: (req, _res, buf) => req.rawBody = buf, perdés los bytes originales. Cuando recalculás el HMAC, no coincide y descartás todos los eventos como inválidos. Tu bot nunca responde, y los logs no muestran nada raro.
Error 3 — No implementar idempotencia. “Es que Meta no debería reintentar”. Sí debería. Y lo hace. Sin dedupe, terminás con respuestas duplicadas y CRM duplicado. Es un bug que escala con tu tráfico.
Error 4 — Olvidar la rotación de CA de Meta (marzo 2026). Este es nuevo y silencioso. Meta dejó de firmar sus certificados con DigiCert y ahora usa su propia CA. Si tu servidor no confía en la nueva CA, los handshakes TLS fallan y dejás de recibir webhooks sin error visible. Solución: descargar meta-outbound-api-ca-2025-12.pem y agregarlo a tu trust store.
Checklist de un webhook listo para producción
Antes de salir a producción con tu agente, validá esto punto por punto:
- HTTPS con certificado válido (Let’s Encrypt alcanza)
- URL pública accesible desde internet
- GET handshake responde 200 con el
hub.challenge - POST valida
X-Hub-Signature-256sobre el raw body - Comparación de tiempo constante (
timingSafeEqualocompare_digest) - Idempotencia por
messages[].idcon TTL de 24-48h - Webhook devuelve 200 en menos de 500ms (encolar y procesar async)
- Suscripción a los campos correctos (
messagessiempre) - Logs estructurados con
message_id,phone_number_idy timestamp - Monitoreo de errores 4xx/5xx con alertas
- Trust store actualizado con la CA de Meta de 2025-2026
Si todo está tildado, podés salir a producción tranquilo.
Y si no querés hacer todo esto a mano
Configurar webhooks de WhatsApp correctamente es una de las cosas más fáciles de hacer mal y más difíciles de debuggear. En TecnoChat los webhooks están preconfigurados, validados y firmados. Vos conectás tu número, configurás las respuestas, y nosotros nos encargamos del endpoint, la firma, la idempotencia, los reintentos y el monitoreo. En lugar de invertir una semana, tu agente está respondiendo en menos de 30 minutos. Si preferís ir por tu cuenta, este tutorial te deja al 90% del camino.
¿Tu agente de WhatsApp está fallando y no sabés si es el webhook, el modelo o el prompt? Escribime a [email protected] y lo revisamos juntos. A veces es una línea.
Fuentes y referencias
- Meta for Developers — Webhooks for the WhatsApp Business Platform ↗
- Chatarmin — WhatsApp Webhooks: Setup, Security & Scaling (2026 Guide) ↗
- Dualhook — WhatsApp Business Platform Webhook Fields Reference ↗
- João Victor Souza — WhatsApp Cloud API Guide: architecture, webhooks, templates ↗
- Webhook Relay — WhatsApp Cloud API Webhooks: Setup, Verify Token and Payloads ↗
Preguntas frecuentes
¿Qué es un webhook de WhatsApp Business API y para qué sirve?
¿Cuánto cuesta recibir webhooks de WhatsApp?
¿Necesito un servidor propio o puedo usar serverless?
¿Cómo verifico que un webhook realmente viene de Meta y no es un atacante?
¿Por qué mi webhook dejó de recibir eventos de un día para otro en abril 2026?
¿Listo para probarlo en tu negocio?
Crea tu cuenta gratis, conecta tu WhatsApp y ten a tu agente IA respondiendo en menos de 15 minutos.
Empezar gratis →Sigue leyendo
Guía WhatsApp API e IA 2026: Cómo Automatizar Ventas en LATAM
Descubre cómo escalar con WhatsApp API e IA en 2026. Guía para PYMES LATAM: reduce costos, aumenta conversiones un 35% y automatiza el 80%.
WhatsApp Business 2026: Guía para PYMES ante el fin de las respuestas gratuitas
Octubre 2026 cambia las tarifas de WhatsApp API. Aprende a calcular el ROI real y automatiza con IA para proteger tu margen ante los nuevos costos por mensaje.
Automatiza WhatsApp Business 2026: Guía IA sin código antes del cambio de precios
Meta cobra por mensajes en 2026. Aprende a automatizar WhatsApp Business con IA sin código, reduce costos un 40% y consigue una tasa de apertura del 98% en LATAM.