Webhooks
Um webhook é uma URL https sua para a qual a WhatsMarketing envia um POST cada vez que ocorre um evento ao qual você se inscreveu. Eles são criados em Integrações → Webhooks e API (até 10 por organização), com um segredo whsec_… que é exibido uma única vez.
Eventos
| Evento | Quando | Status |
|---|---|---|
contact.created |
Um contato é criado (painel, API, importação ou entrada pelo WhatsApp) | Ativo |
contact.updated |
Os dados de um contato mudam | Ativo |
contact.stage_changed |
Um contato muda de etapa do ciclo de vida | Ativo |
campaign.finished |
Uma campanha termina de ser enviada | Ativo |
campaign.replied |
Um destinatário responde a uma campanha | Em breve |
conversation.opened, conversation.assigned, conversation.closed |
Ciclo de uma conversa | Em breve |
message.received, message.sent |
Mensagens recebidas e enviadas | Em breve |
automation.run |
Uma automação é executada | Em breve |
bot.handoff, bot.message |
Um agente de IA transfere para uma pessoa ou responde | Em breve |
Você já pode se inscrever nos eventos «em breve»: eles começarão a chegar quando forem ativados, sem mexer em nada.
O que você recebe
POST /sua-rota 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…{
"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 e X-WM-Delivery identificam a entrega: se você receber duas vezes o mesmo valor (por uma nova tentativa), processe só a primeira. data traz o essencial para saber o que aconteceu; para a ficha completa, peça GET /contacts/{contactId}.
| Evento | data |
|---|---|
contact.created |
organizationId, contactId, fullName, primaryPhone, primaryEmail, lifecycleStage |
contact.updated |
o mesmo mais changedFields (os campos que mudaram) |
contact.stage_changed |
o mesmo mais previousStage |
campaign.finished |
organizationId, campaignId, name, status, sent, failed |
Verificar a assinatura
A assinatura protege contra alguém enviar eventos falsos para você. É o HMAC-SHA256 do texto "<t>.<corpo bruto>" com o seu segredo, em hexadecimal. Calcule com o corpo exatamente como chegou (sem reformatar o JSON), compare em tempo constante e descarte marcas de tempo com mais de 5 minutos.
import crypto from 'node:crypto';
function verificar(corpoBruto, cabecalho, segredo) {
const partes = Object.fromEntries(cabecalho.split(',').map((p) => p.split('=')));
const esperado = crypto.createHmac('sha256', segredo).update(`${partes.t}.${corpoBruto}`).digest('hex');
const recente = Math.abs(Date.now() / 1000 - Number(partes.t)) < 300;
return recente && crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1));
}
// Express: use o corpo bruto, não o JSON já 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(); // responda já; processe depois
const evento = JSON.parse(req.body);
// …
});import hmac, hashlib, time
def verificar(corpo_bruto: bytes, cabecalho: str, segredo: str) -> bool:
partes = dict(p.split("=") for p in cabecalho.split(","))
esperado = hmac.new(segredo.encode(), f"{partes['t']}.".encode() + corpo_bruto, hashlib.sha256).hexdigest()
recente = abs(time.time() - int(partes["t"])) < 300
return recente and hmac.compare_digest(esperado, partes["v1"])<?php
function verificar(string $corpoBruto, string $cabecalho, string $segredo): bool {
parse_str(str_replace(',', '&', $cabecalho), $partes);
$esperado = hash_hmac('sha256', $partes['t'] . '.' . $corpoBruto, $segredo);
$recente = abs(time() - (int) $partes['t']) < 300;
return $recente && hash_equals($esperado, $partes['v1']);
}Novas tentativas e pausa
- Consideramos recebida qualquer resposta
2xxem menos de 10 segundos. - Se falhar, tentamos de novo 5 vezes: em 1 minuto, em 5 minutos, em 30, em 2 horas e em 12 horas.
- Dez entregas esgotadas seguidas pausam o webhook. Você o reativa pelo painel; de lá também pode tentar novamente as que falharam.
- As entregas ficam guardadas por 7 dias: no painel você vê cada uma com a sua resposta.
Testar localmente
- Suba o seu servidor localmente.
- Abra um túnel (
ngrok http 3000ou Cloudflare Tunnel) e copie a URLhttps. - Crie o webhook com essa URL e
contact.created. - Crie um contato pelo painel ou com
POST /contacts: em segundos você verá oPOSTno seu console.