Como registrar, validar assinatura HMAC e garantir entrega idempotente de eventos em tempo real.
Webhook é HTTP POST que a VZD envia para seu servidor toda vez que algo acontece: cliente envia mensagem, confirma entrega, responde pelo celular (echo), ou template muda de estado.
Sem webhook você depende de polling constante (GET /messages, GET /statuses) — latência alta e ineficiente. Com webhook você recebe em milissegundos.
curl -X PUT https://api.vzdapplication.com/api/whatsapp/webhook \
-H "Authorization: Bearer vzd_SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://seu-dominio.com.br/webhooks/whatsapp"
}'
VZD retorna um segredo (aparece uma única vez):
{
"webhook_url": "https://seu-dominio.com.br/webhooks/whatsapp",
"webhook_secret": "sk_whk_abc123..."
}
Seu servidor receberá:
POST /webhooks/whatsapp HTTP/1.1
Host: seu-dominio.com.br
X-VZD-Signature: sha256=abc123def456...
Content-Type: application/json
Content-Length: 512
{
"event": "whatsapp.message.received",
"event_id": "evt_9f2c0a1b",
"occurred_at": "2024-01-15T10:00:00.000Z",
"channel": {
"phone_number_id": "5511999887766",
"waba_id": "100234567890"
},
"data": { ... }
}
Antes de processar qualquer evento, valide que veio de fato da VZD usando o segredo e o header X-VZD-Signature.
const crypto = require('crypto');
const payload = req.rawBody; // corpo bruto em bytes
const signature = req.headers['x-vzd-signature'];
const secret = process.env.WEBHOOK_SECRET;
// Extrai hex da assinatura
const [algorithm, hash] = signature.split('=');
if (algorithm !== 'sha256') {
return res.status(401).send('Unknown algorithm');
}
// Computa HMAC
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Compara em tempo constante
const isValid = crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(hash)
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// OK, processa evento
handleWebhookEvent(req.body);
import hmac
import hashlib
from flask import request
payload = request.data # corpo bruto em bytes
signature = request.headers.get('X-VZD-Signature')
secret = os.environ['WEBHOOK_SECRET']
algorithm, hash_hex = signature.split('=')
if algorithm != 'sha256':
return 'Unknown algorithm', 401
computed = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
# Compara em tempo constante
if not hmac.compare_digest(computed, hash_hex):
return 'Invalid signature', 401
# OK, processa evento
handle_webhook_event(request.json)
JSON.stringify(request.body) muda espaçamento e ordem de chaves. Use o corpo bruto. Se você estiver usando um framework que já parsou JSON, capture o corpo antes do parse.
VZD envia eventos com ID determinístico. Se perder a confirmação, reenviará o mesmo evento com o mesmo ID.
Para garantir idempotência:
event_id em um banco de dados (até 7 dias)
const eventId = req.body.event_id;
// Consulta banco
const exists = await db.events.findOne({ id: eventId });
if (exists) {
// Já processado
return res.status(200).send('OK');
}
// Processa
await handleEvent(req.body);
// Grava
await db.events.create({ id: eventId, processedAt: new Date() });
res.status(200).send('OK');
VZD espera resposta HTTP 200-299 em segundos. Processe o evento em background.
// Responde logo
res.status(200).send('OK');
// Processa depois
process.nextTick(async () => {
await updateCRMWithMessage(req.body.data);
});
| Evento | Quando? | Campo data |
|---|---|---|
whatsapp.message.received |
Cliente final enviou mensagem | { wamid, from, type, text/media, ... } |
whatsapp.message.echoed |
Alguém respondeu pelo celular (Coexistência) | { wamid, from, direction: "OUTBOUND", origin: "BUSINESS_APP_ECHO", text, ... } |
whatsapp.message.status |
Status de entrega (SENT, DELIVERED, READ, FAILED) | { wamid, status, failureReason } |
whatsapp.template.status |
Template mudou de estado (APPROVED, REJECTED, PAUSED, ...) | { template_name, status, rejection_reason } |
{
"event": "whatsapp.message.received",
"event_id": "evt_9f2c0a1b",
"occurred_at": "2024-01-15T10:00:00.000Z",
"channel": {
"phone_number_id": "5511999887766",
"waba_id": "100234567890"
},
"data": {
"wamid": "wamid.HhBmRGVmAUH7AglMkZGVzQ==",
"from": "5511987654321",
"direction": "INBOUND",
"origin": "CLOUD_API",
"type": "text",
"text": "Olá! Qual é o horário?",
"timestamp": "2024-01-15T10:00:00.000Z",
"sentAt": "2024-01-15T10:00:00.000Z"
}
}
Armadilha crítica: se você tem IA respondendo automaticamente, ela pode responder ao próprio echo (resposta do celular).
A solução é filtrar por origin antes de entregar:
if (event.data.origin === 'BUSINESS_APP_ECHO') {
// Cliente respondeu pelo celular — ignorar para IA
return res.status(200).send('OK');
}
// Só aqui chega a IA
await aiService.respond(event.data);