Tutorial dev

Tutorial: webhook de WhatsApp en Node.js (código completo)

Te muestro desde cero cómo recibir mensajes de WhatsApp vía Cloud API de Meta usando Node.js + Express, validar la firma HMAC para seguridad y procesar diferentes tipos de mensaje (texto, audio, imagen, estado). Código probado en producción, listo para copiar.

15 de mayo de 2026 · 12 min de lectura · MercaBot

Requisitos previos

Paso 1 — Setup del proyecto

mkdir wa-webhook && cd wa-webhook
npm init -y
npm install express
# crypto viene con Node core, no hay que instalar
touch server.js .env

Archivo .env:

WHATSAPP_TOKEN=EAAxxxx...
APP_SECRET=abc123...
PHONE_NUMBER_ID=109876543210987
VERIFY_TOKEN=mi_token_secreto_aleatorio_xyz
PORT=3000

Paso 2 — Endpoint de verificación (GET)

Cuando registras el webhook en Business Manager, Meta envía un GET con hub.challenge. Tu endpoint necesita devolver exactamente ese valor, probando que eres "dueño" del endpoint.

// server.js
import express from 'express';
import crypto from 'crypto';

const app = express();

// IMPORTANTE: necesitamos el raw body para validar la firma después
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }
}));

const { WHATSAPP_TOKEN, APP_SECRET, PHONE_NUMBER_ID, VERIFY_TOKEN, PORT = 3000 } = process.env;

// Verificación inicial del webhook
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 === VERIFY_TOKEN) {
    console.log('Webhook verificado');
    return res.status(200).send(challenge);
  }
  return res.sendStatus(403);
});

Paso 3 — Endpoint de recepción (POST)

Aquí llegan los eventos: mensaje nuevo, estado de envío, cambio de perfil, etc. Responde siempre 200 rápido (en <5 segundos), si no Meta reintenta y puede banear tu webhook.

// Recepción de eventos
app.post('/webhook', (req, res) => {
  // 1) Validar firma antes de cualquier cosa
  if (!verificarFirma(req)) {
    console.warn('Firma inválida — rechazando');
    return res.sendStatus(401);
  }

  // 2) Responder 200 ya (procesar async)
  res.sendStatus(200);

  // 3) Procesar evento
  procesarEvento(req.body).catch(err => console.error('Error procesando:', err));
});

function verificarFirma(req) {
  const sig = req.get('x-hub-signature-256');
  if (!sig) return false;
  const esperado = 'sha256=' + crypto
    .createHmac('sha256', APP_SECRET)
    .update(req.rawBody)
    .digest('hex');
  // crypto.timingSafeEqual evita timing attack
  try {
    return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(esperado));
  } catch { return false; }
}

Paso 4 — Procesar el mensaje

El payload de Meta tiene estructura anidada. Aquí está el parser para los 4 tipos más comunes:

async function procesarEvento(body) {
  // Payload: { object, entry: [{ changes: [{ value: { ... } }] }] }
  const entry = body.entry?.[0];
  const change = entry?.changes?.[0]?.value;
  if (!change) return;

  // a) Mensaje recibido
  const msgs = change.messages || [];
  for (const msg of msgs) {
    const from = msg.from; // número del remitente
    const id = msg.id;

    if (msg.type === 'text') {
      const texto = msg.text.body;
      console.log(`📩 ${from}: ${texto}`);
      await responder(from, `Recibí: "${texto}"`);
    }
    else if (msg.type === 'image' || msg.type === 'audio' || msg.type === 'video' || msg.type === 'document') {
      const mediaId = msg[msg.type].id;
      console.log(`📷 ${from} envió ${msg.type} id=${mediaId}`);
      // Para descargar el archivo: GET https://graph.facebook.com/v21.0/{mediaId}
      // (con Bearer token) → devuelve URL con TTL corto para descarga
    }
    else if (msg.type === 'button' || msg.type === 'interactive') {
      // Botón de plantilla o lista interactiva clickeado
      console.log(`🔘 ${from} clickeó un botón`);
    }
  }

  // b) Estado de envío (delivered, read, failed)
  const statuses = change.statuses || [];
  for (const st of statuses) {
    console.log(`✓ status msg=${st.id} status=${st.status}`);
  }
}

Paso 5 — Enviar mensaje de vuelta

async function responder(to, texto) {
  const url = `https://graph.facebook.com/v21.0/${PHONE_NUMBER_ID}/messages`;
  const body = {
    messaging_product: 'whatsapp',
    recipient_type: 'individual',
    to,
    type: 'text',
    text: { body: texto, preview_url: false }
  };
  const r = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${WHATSAPP_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  });
  if (!r.ok) {
    console.error('Error enviando:', r.status, await r.text());
  }
}

app.listen(PORT, () => console.log(`🟢 Webhook escuchando en el puerto ${PORT}`));

Paso 6 — Deploy

Render es el más simple para probar (free tier alcanza). Alternativas: Railway, Fly.io, AWS Lambda.

  1. Crea un repo en GitHub con el código.
  2. En render.com: New → Web Service → conecta al repo.
  3. Build command: npm install. Start command: node server.js.
  4. Agrega las variables de entorno (WHATSAPP_TOKEN, APP_SECRET, etc.).
  5. Deploy. Toma la URL pública (ej.: https://wa-webhook.onrender.com).

Paso 7 — Registrar el webhook en Business Manager

  1. developers.facebook.com → tu app → WhatsApp → Configuration.
  2. En "Webhook", haz clic en Configure.
  3. Callback URL: https://wa-webhook.onrender.com/webhook.
  4. Verify Token: el mismo valor de VERIFY_TOKEN de tu .env.
  5. Haz clic en Verify and Save. Meta hace GET → tu endpoint responde el challenge → registrado.
  6. En "Webhook fields", haz clic en Manage y activa al menos messages y message_template_status_update.

Probando

  1. Envía un mensaje desde tu celular personal al número WhatsApp de la empresa.
  2. Los logs del servidor deben mostrar: 📩 5491...: hola test.
  3. Deberías recibir de vuelta: Recibí: "hola test".

Errores comunes

Cuándo vale la pena hacerlo desde cero (y cuándo no)

Construir desde cero tiene sentido si eres dev construyendo un SaaS con WhatsApp embebido en el producto (ej.: app de delivery, fintech enviando notificaciones). Necesitas control total del código, integración con microservicios internos, deploy propio.

No tiene sentido si solo quieres atender clientes en el WhatsApp de la empresa — gastar 2-4 semanas implementando panel multi-agente, bot IA, broadcast HSM y plantillas desde cero es trabajo perdido. Una plataforma como MercaBot lo resuelve en 3 minutos por $39/mes.

Atención WhatsApp sin código

Si el objetivo es atender al cliente (no construir producto), saltea la parte del webhook. MercaBot conecta en 3 minutos con panel listo + bot IA + broadcast.

Probar gratis →