Webhooks
Um único endpoint de produção recebe eventos operacionais da Meta e eventos internos da HookCloud.
Arquitetura de entrega
A URL salva no Partner Portal é aplicada às instâncias ativas. Quando a URL muda, a HookCloud migra os callbacks na Meta e mostra o progresso.
Referências: Webhooks Meta · Override de callback
Desafio GET da Meta
app.get('/webhooks/whatsapp', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === process.env.META_VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
return res.sendStatus(403);
});Eventos da Meta com e sem telefone
A Meta envia os campos assinados no app. Preserve o payload de cada campo para o uso que seu produto oferece, inclusive business_username_updates, eventos de templates e coexistência. Não transforme todos os eventos em mensagens.
// Execute após verificar a origem/autenticidade do webhook.
// Persistir na fila deve terminar antes do HTTP 200.
export function parseMetaEvents(payload) {
const events = [];
for (const entry of payload.entry ?? []) {
for (const change of entry.changes ?? []) {
const value = change.value ?? {};
const base = { wabaId: entry.id, phoneNumberId: value.metadata?.phone_number_id };
if (change.field !== 'messages') {
events.push({ ...base, kind: 'meta_event', field: change.field, value });
continue;
}
for (const message of value.messages ?? []) {
const contact = (value.contacts ?? []).find(c =>
(message.from_user_id && c.user_id === message.from_user_id) ||
(message.from && c.wa_id === message.from));
events.push({ ...base, kind: 'inbound', message,
userId: message.from_user_id ?? contact?.user_id ?? null,
parentUserId: message.from_parent_user_id ?? contact?.parent_user_id ?? null,
username: contact?.profile?.username ?? null,
phone: message.from || contact?.wa_id || null });
}
for (const status of value.statuses ?? []) {
events.push({ ...base, kind: 'status', status,
userId: status.recipient_user_id ?? null,
parentUserId: status.recipient_parent_user_id ?? null,
phone: status.recipient_id || null });
}
}
}
return events;
}O parser acima é um adaptador de dados. O receptor HTTP precisa validar a origem/assinatura conforme a integração, gravar os eventos de forma durável e então responder 200. Responder antes da gravação pode perder o evento se o processo cair. O hub.verify_token do desafio GET não valida a assinatura HMAC de um POST.
Para mensagens, deduplique pelo wamid dentro da instância. Para status, use também o estado e preserve a progressão sem regredir de read para sent se eventos chegarem fora de ordem. Não procure uma pessoa apenas pelo telefone; ele pode estar ausente.
Referência: Campos e exceções dos webhooks de identidade.
Eventos HookCloud e HMAC
A HookCloud inclui os headers abaixo. Verifique a assinatura usando o corpo bruto e rejeite timestamps antigos para reduzir replay.
| Header | Descrição |
|---|---|
X-HookCloud-Event | Tipo do evento |
X-HookCloud-Delivery | ID único da entrega |
X-HookCloud-Timestamp | Unix timestamp |
X-HookCloud-Signature | sha256=HMAC_SHA256(secret, timestamp.rawBody) |
import crypto from 'node:crypto';
export function verifyHookCloudWebhook({ rawBody, timestamp, signature, secret }) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const received = String(signature || '').replace(/^sha256=/, '');
if (expected.length !== received.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}Evento de template
{
"id": "UUID_DO_EVENTO",
"type": "hookcloud.meta.template.updated",
"created_at": "2026-07-21T12:00:00Z",
"partner_id": "UUID_DO_PARTNER",
"data": {
"meta_waba_id": "WABA_ID",
"template_id": "123456789",
"name": "confirmacao_agendamento",
"language": "pt_BR",
"previous_status": "PENDING",
"status": "APPROVED",
"category": "UTILITY",
"quality_status": "GREEN",
"sendable": true
}
}Use id para idempotência. Ao receber a mudança, atualize a tela e reconcilie com list-partner-meta-templates.
Troca segura da URL
- Implemente GET e POST na URL nova.
- Teste com
validate-partner-webhook-endpoint. - Aplique com
update-partner-webhook-endpoint. - Acompanhe a migração.
- Reprocesse somente as falhas.
Boas práticas de produção
- Responder HTTP 2xx em poucos segundos
- Enfileirar processamento pesado
- Deduplicar pelo ID da mensagem e ID da entrega
- Não seguir redirects no seu validador
- Registrar falhas sem conteúdo sensível
- Monitorar taxa de HTTP 4xx/5xx
- Rotacionar segredos com processo controlado
