Webhooks

Configurar webhooks

Recibe en tu servidor los eventos de Whapi, verifica que vienen de Whapi y responde para que no se reintenten.

Un webhook es un POST que Whapi envía a una URL de tu servidor cada vez que pasa algo en tu cuenta. Es el camino inverso a la API: en lugar de consultar, recibes el aviso.

Eventos

EventoCuándo llegaGuía
lead.createdSe creó un lead, a mano o automáticamente.Leads, contactos y empresas
lead.updatedCambió un lead, sus contactos, canales o campos personalizados, desde Whapi.Leads, contactos y empresas
waba.message.receivedUn cliente escribió a una de tus líneas de WhatsApp Business API.Recibir mensajes y estados
waba.message.statusCambió el estado de un mensaje que enviaste con la API.Recibir mensajes y estados
waba.template.statusMeta aprobó, rechazó o pausó una plantilla.Plantillas

Configurar un webhook

  1. En Whapi, entra a Configuración de la empresa → Webhooks. Solo los administradores ven esta pestaña.
  2. En Secreto de firma, copia el secreto (empieza con whsec_) y guárdalo en tu servidor.
  3. Pulsa Agregar webhook, escribe tu URL y elige el evento. Cada webhook escucha un solo evento, y cada evento admite un solo webhook: para recibir varios eventos, crea un webhook por evento (pueden apuntar a la misma URL).
  4. Pulsa Probar para recibir un evento de ejemplo.

La URL tiene que usar HTTPS.

Qué recibes

Cada evento llega como JSON con el mismo sobre:

{
  "apiVersion": "2026-06-15",
  "id": "3b241101-e2bb-4255-8caf-4136c566a962",
  "type": "lead.created",
  "createdAt": "2026-10-14T15:04:05.000Z",
  "data": { }
}

data cambia según el evento: su forma completa está en la referencia.

Y con estos headers:

HeaderContenido
X-Whapi-Event-IdEl id del evento. Se repite en los reintentos: úsalo para no procesar dos veces lo mismo.
X-Whapi-Event-TypeEl tipo de evento.
X-Whapi-TimestampMomento del envío, en segundos Unix.
X-Whapi-Signaturesha256= seguido de la firma en hexadecimal.

Verificar la firma

Antes de procesar un evento, comprueba que lo envió Whapi. La firma es un HMAC-SHA256 con tu secreto de firma sobre el texto "{X-Whapi-Timestamp}.{body}":

  • body son los bytes exactos que recibiste. No vuelvas a serializar el JSON: cualquier diferencia de espacios u orden cambia la firma.
  • El secreto se usa completo, con el prefijo whsec_, como texto.

Node.js (Express):

const crypto = require("crypto");

function verifyWhapiWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-whapi-timestamp"];
  const sigHeader = headers["x-whapi-signature"] || "";
  if (!timestamp || !sigHeader.startsWith("sha256=")) return false;

  // Rechaza eventos de hace más de 5 minutos (evita que reenvíen uno viejo).
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = sigHeader.slice("sha256=".length);
  const computed = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody.toString("utf8")}`, "utf8")
    .digest("hex");

  const a = Buffer.from(computed, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw en esta ruta, antes de express.json, para conservar el body original.
app.post("/webhooks/whapi", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyWhapiWebhook(req.body, req.headers, process.env.WHAPI_WEBHOOK_SECRET)) {
    return res.status(401).send("Firma inválida");
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // Procesa el evento (idealmente en segundo plano) y responde enseguida.
  res.sendStatus(200);
});

Python:

import hashlib
import hmac
import time

def verify_whapi_webhook(raw_body: bytes, timestamp: str, signature_header: str, secret: str) -> bool:
    if not signature_header.startswith("sha256="):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False

    expected = signature_header.removeprefix("sha256=")
    signed_payload = f"{timestamp}.{raw_body.decode('utf-8')}"
    computed = hmac.new(secret.encode("utf-8"), signed_payload.encode("utf-8"), hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, expected)

Responder y reintentos

Responde con cualquier 2xx en menos de 15 segundos. Si tu proceso tarda más, guarda el evento, responde y procésalo después.

Tu respuestaQué hace Whapi
2xxEntregado.
408, 429, 5xx, timeout o error de redReintenta: hasta 5 intentos en total, con esperas crecientes desde 1 minuto.
Otro 4xxLo da por fallido y no reintenta.

Como un evento puede llegar más de una vez, descarta los repetidos con X-Whapi-Event-Id.

En la pestaña Webhooks ves cada entrega con su resultado, el payload enviado y un botón Reintentar para las fallidas.

Evento de prueba

Probar envía un ejemplo del evento del webhook, firmado como uno real y con "test": true dentro de data. Se envía una sola vez, sin reintentos. Para los eventos de leads usa el último lead de tu cuenta, así que necesitas al menos uno.

Si la firma no coincide

  1. ¿Usas el secreto completo, con whsec_?
  2. ¿Firmas timestamp + "." + body, y no solo el body?
  3. ¿Usas el body original, y no el JSON vuelto a serializar después de parsearlo?
  4. ¿Tomas el timestamp del header X-Whapi-Timestamp?
  5. ¿Comparas sin el prefijo sha256=?
  6. ¿Regeneraste el secreto en Whapi y no lo actualizaste en tu servidor? Al regenerarlo, el anterior deja de valer de inmediato.