Ir al contenido
WhatsMarketingDeveloper Hub

Webhooks

Un webhook es una URL https tuya a la que WhatsMarketing envía un POST cada vez que ocurre un evento al que te suscribiste. Se crean en Integraciones → Webhooks y API (hasta 10 por organización), con un secreto whsec_… que se muestra una sola vez.

Eventos

Evento Cuándo Estado
contact.created Se crea un contacto (panel, API, importación o entrada por WhatsApp) Activo
contact.updated Cambian los datos de un contacto Activo
contact.stage_changed Un contacto cambia de etapa del ciclo de vida Activo
campaign.finished Una campaña termina de enviarse Activo
campaign.replied Un destinatario responde a una campaña Próximamente
conversation.opened, conversation.assigned, conversation.closed Ciclo de una conversación Próximamente
message.received, message.sent Mensajes entrantes y salientes Próximamente
automation.run Se ejecuta una automatización Próximamente
bot.handoff, bot.message Un agente IA deriva a una persona o responde Próximamente

Puedes suscribirte ya a los eventos «próximamente»: empezarán a llegar cuando se activen, sin tocar nada.

Qué recibes

HTTP
POST /tu-ruta HTTP/1.1
Content-Type: application/json
User-Agent: WhatsMarketing-Webhooks/1.0
X-WM-Event: contact.created
X-WM-Delivery: 3f4e7f0a-9b1c-4d2e-8f3a-4b5c6d7e8f90
X-WM-Signature: t=1760436060,v1=5d41402abc4b2a76b9719d911017c592…
JSON
{
  "id": "3f4e7f0a-9b1c-4d2e-8f3a-4b5c6d7e8f90",
  "event": "contact.created",
  "createdAt": "2026-10-14T10:21:00.000Z",
  "data": {
    "organizationId": "f0e1d2c3-b4a5-4968-8776-655443322110",
    "contactId": "0b1c2d3e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
    "fullName": "Laura Pérez",
    "primaryPhone": "+34612345678",
    "primaryEmail": "laura@ejemplo.com",
    "lifecycleStage": "Lead nuevo"
  }
}

id y X-WM-Delivery identifican la entrega: si recibes dos veces el mismo valor (por un reintento), procesa solo la primera. data trae lo justo para saber qué pasó; para la ficha completa pide GET /contacts/{contactId}.

Evento data
contact.created organizationId, contactId, fullName, primaryPhone, primaryEmail, lifecycleStage
contact.updated lo mismo más changedFields (los campos que cambiaron)
contact.stage_changed lo mismo más previousStage
campaign.finished organizationId, campaignId, name, status, sent, failed

Verificar la firma

La firma protege de que alguien te mande eventos falsos. Es HMAC-SHA256 del texto "<t>.<cuerpo crudo>" con tu secreto, en hexadecimal. Calcula con el cuerpo tal como llegó (sin reformatear el JSON), compara en tiempo constante y descarta marcas de tiempo de más de 5 minutos.

Node.js
import crypto from 'node:crypto';

function verificar(cuerpoCrudo, cabecera, secreto) {
  const partes = Object.fromEntries((cabecera || '').split(',').map((p) => p.split('=')));
  if (!partes.t || !partes.v1) return false;
  const esperado = Buffer.from(crypto.createHmac('sha256', secreto).update(`${partes.t}.${cuerpoCrudo}`).digest('hex'));
  const recibido = Buffer.from(partes.v1);
  const fresco = Math.abs(Date.now() / 1000 - Number(partes.t)) < 300;
  return fresco && esperado.length === recibido.length && crypto.timingSafeEqual(esperado, recibido);
}

// Express: usa el cuerpo crudo, no el JSON ya parseado.
app.post('/webhooks/wm', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verificar(req.body.toString(), req.header('X-WM-Signature'), process.env.WM_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  res.status(200).end(); // responde ya; procesa después
  const evento = JSON.parse(req.body);
  // …
});
Python
import hmac, hashlib, time

def verificar(cuerpo_crudo: bytes, cabecera: str, secreto: str) -> bool:
    partes = dict(p.split("=", 1) for p in (cabecera or "").split(",") if "=" in p)
    if "t" not in partes or "v1" not in partes:
        return False
    esperado = hmac.new(secreto.encode(), f"{partes['t']}.".encode() + cuerpo_crudo, hashlib.sha256).hexdigest()
    fresco = abs(time.time() - int(partes["t"])) < 300
    return fresco and hmac.compare_digest(esperado, partes["v1"])
PHP
<?php
function verificar(string $cuerpoCrudo, string $cabecera, string $secreto): bool {
  parse_str(str_replace(',', '&', (string) $cabecera), $partes);
  if (empty($partes['t']) || empty($partes['v1'])) return false;
  $esperado = hash_hmac('sha256', $partes['t'] . '.' . $cuerpoCrudo, $secreto);
  $fresco = abs(time() - (int) $partes['t']) < 300;
  return $fresco && hash_equals($esperado, $partes['v1']);
}

Reintentos y pausa

  • Contamos como recibido cualquier respuesta 2xx en menos de 10 segundos.
  • Si falla, reintentamos 5 veces: al minuto, a los 5 minutos, a los 30, a las 2 horas y a las 12 horas.
  • Diez entregas agotadas seguidas pausan el webhook. Lo reactivas desde el panel; también puedes reintentar las fallidas desde ahí.
  • Las entregas se guardan 7 días: en el panel ves cada una con su respuesta.

Probar en local

  1. Levanta tu servidor en local.
  2. Abre un túnel (ngrok http 3000 o Cloudflare Tunnel) y copia la URL https.
  3. Crea el webhook con esa URL y contact.created.
  4. Crea un contacto desde el panel o con POST /contacts: en segundos verás el POST en tu consola.