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.

MP
Mateo Picasso
Lead Technical Writer · IA & Automatización
· · ⏱ 11 min lectura
Diagrama técnico mostrando el flujo de un webhook de WhatsApp Business API desde Meta hasta un endpoint HTTPS con verificación HMAC-SHA256

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_token y hub.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:

  1. Número de WhatsApp Business API activo, ya conectado a tu WABA.
  2. App en Meta for Developers con el producto WhatsApp agregado (te da un App ID y un App Secret).
  3. Phone Number ID y WABA ID, que aparecen en WhatsApp Manager > Phone Numbers.
  4. Endpoint HTTPS público con certificado válido (VPS, Lambda, Cloudflare Worker, etc). En dev, ngrok o cloudflared para exponer localhost.
  5. 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:

  1. Entrá a developers.facebook.com y seleccioná tu app.
  2. WhatsApp > Configuration > Webhook > Edit.
  3. En Callback URL, pegá tu URL: https://tu-dominio.com/webhook.
  4. En Verify Token, escribí un string aleatorio que vas a guardar en tu variable de entorno.
  5. 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:

CampoPara qué sirve
messagesEsencial. Mensajes entrantes y estados de salida. Sin este, no hay bot.
message_template_status_updateCuando Meta aprueba, rechaza o marca como spam una plantilla.
message_template_quality_updateCambios en calidad de plantillas (HIGH/MEDIUM/LOW).
phone_number_quality_updateCambios en calidad de tu número (GREEN/YELLOW/RED).
account_updateWABA: 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-256 sobre el raw body
  • Comparación de tiempo constante (timingSafeEqual o compare_digest)
  • Idempotencia por messages[].id con TTL de 24-48h
  • Webhook devuelve 200 en menos de 500ms (encolar y procesar async)
  • Suscripción a los campos correctos (messages siempre)
  • Logs estructurados con message_id, phone_number_id y 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

Preguntas frecuentes

¿Qué es un webhook de WhatsApp Business API y para qué sirve?

Es un endpoint HTTPS en tu servidor donde Meta te entrega en tiempo real cada evento de tu número: mensajes entrantes, estados de entrega, lecturas, clics en botones y aprobaciones de plantillas. Sin un webhook configurado, tu agente no recibe ni una sola conversación. Todo el modelo de WhatsApp Cloud API es push, no pull: si no tenés una URL pública escuchando, no hay nada.

¿Cuánto cuesta recibir webhooks de WhatsApp?

Recibir webhooks es gratis. No hay tarifa de Meta por evento entregado a tu endpoint. Lo que sí cuesta es cada mensaje que TÚ envías al cliente (utility cerca de $0.004 USD, marketing cerca de $0.025 USD, según el país), pero los eventos que Meta te entrega no tienen cobro por evento. Eso sí: si tu endpoint falla o responde lento, Meta te penaliza con reintentos y eventualmente suspende la entrega.

¿Necesito un servidor propio o puedo usar serverless?

Podés usar lo que sea, siempre que cumpla tres reglas: (1) HTTPS con certificado válido (no self-signed en producción), (2) responder 200 OK en menos de 5 segundos, (3) URL pública accesible desde Internet. Serverless en AWS Lambda, Vercel, Cloudflare Workers o Google Cloud Functions funciona perfecto. La trampa típica: probar en localhost con ngrok funciona en dev, pero en producción necesitás un dominio real con TLS.

¿Cómo verifico que un webhook realmente viene de Meta y no es un atacante?

Meta firma cada POST con el header X-Hub-Signature-256. Es un HMAC-SHA256 del cuerpo crudo (raw body) usando tu App Secret como clave. Tu servidor recalcula esa firma sobre los bytes exactos que llegaron y compara en tiempo constante con crypto.timingSafeEqual (Node) o hmac.compare_digest (Python). Si no coincide, rechazás con 401. Nunca confíes en una comparación con ==, es vulnerable a timing attacks.

¿Por qué mi webhook dejó de recibir eventos de un día para otro en abril 2026?

El 31 de marzo de 2026 Meta rotó su Certificate Authority: dejó de firmar con DigiCert y empezó a firmar con su propia CA (meta-outbound-api-ca-2025-12.pem). Si tu servidor no confía en esa nueva CA, los handshakes TLS fallan y dejás de recibir webhooks sin error visible en el dashboard. Solución: descargar el bundle nuevo y agregarlo a tu trust store. Es un cambio silencioso que rompió a varios equipos en abril.

¿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

¿Necesitas ayuda?