Tema
Webhooks
Webhooks avisam o seu sistema na hora em que algo acontece no WhatsApp da empresa.
Cadastrar
No painel, Integrações → Novo webhook (ou POST /api/v1/webhooks com uma chave webhooks:manage): informe uma URL https:// pública e escolha os eventos. Guarde o segredo (whsec_...) mostrado — ele aparece uma vez e serve para verificar a assinatura.
Os eventos levam o texto das mensagens
Aponte webhooks só para sistemas da sua empresa. Endereços privados (localhost, 10.x, 192.168.x…) são recusados.
Eventos
| Evento | Quando |
|---|---|
message.received | Um cliente mandou mensagem |
message.sent | Saiu uma mensagem da empresa (painel, robô, API, MCP ou o próprio celular) |
message.status_updated | API oficial: uma mensagem enviada foi entregue (delivered), lida (read) ou falhou (failed) |
conversation.updated | Mudou o responsável ou o status da conversa (open, pending, resolved) |
conversation.rated | O cliente respondeu a pesquisa de satisfação (score de 1 a 5) |
connection.updated | O WhatsApp conectou, desconectou ou está conectando |
conversation.handoff_requested | O robô passou a conversa para a equipe: o cliente pediu uma pessoa ou o robô não soube responder |
Formato
http
POST /seu-endpoint
Content-Type: application/json
User-Agent: FaliaWebhooks/0.3.0
X-Proton-Event: message.received
X-Proton-Signature: t=1730000000000,v1=5f2b...
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-a3ce929d0e0e4736-01
{
"id": "evt_8c1d...",
"type": "message.received",
"created_at": 1730000000000,
"company_id": 7,
"data": { "id": "3EB0...", "conversation_id": "5511999990000@s.whatsapp.net", "from_me": false, "text": "Quero um orçamento", "sent_at": 1730000000000 }
}connection.updated traz data: { "state": "open" | "close" | "connecting" }.
message.status_updated traz data: { "id": "wamid...", "conversation_id": "5511999990000@s.whatsapp.net", "status": "failed", "error": "131047: Re-engagement message" } (error é null quando não falhou). Só existe para empresas na API oficial.
conversation.handoff_requested traz data: { "conversation_id": "5511999990000@s.whatsapp.net", "reason": "cliente_pediu" | "robo_nao_soube" }. O robô fica calado nessa conversa até alguém clicar em Retomar robô no painel.
Responda 2xx em até 10 s (processe depois, numa fila). Se falhar, tentamos de novo em 1 min, 5 min, 30 min, 2 h e 12 h; depois a entrega fica como falhou e pode ser reenviada no painel (Integrações → Entregas → Reenviar). A entrega é pelo menos uma vez: use o id do evento para não processar duas vezes. Registre o traceparent nos seus logs.
Verificar a assinatura
Calcule HMAC-SHA256(segredo, "<t>.<corpo bruto>") em hexadecimal e compare com v1. Rejeite se t tiver mais de 5 minutos.
js
import crypto from 'node:crypto'
export function verify(secret, header, rawBody) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')))
if (Math.abs(Date.now() - Number(t)) > 5 * 60_000) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}python
import hashlib, hmac, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() * 1000 - int(parts["t"])) > 5 * 60_000:
return False
msg = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])apex
public static Boolean verify(String secret, String header, String rawBody) {
Map<String, String> p = new Map<String, String>();
for (String kv : header.split(',')) { p.put(kv.substringBefore('='), kv.substringAfter('=')); }
Long t = Long.valueOf(p.get('t'));
if (Math.abs(System.currentTimeMillis() - t) > 5 * 60 * 1000) return false;
Blob mac = Crypto.generateMac('hmacSHA256', Blob.valueOf(p.get('t') + '.' + rawBody), Blob.valueOf(secret));
return EncodingUtil.convertToHex(mac) == p.get('v1');
}php
function verify(string $secret, string $header, string $rawBody): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(round(microtime(true) * 1000) - (int)$p['t']) > 300000) return false;
$expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $p['v1']);
}