Webhook é a espinha dorsal de qualquer integração WhatsApp séria — é por ele que sua aplicação recebe mensagens em tempo real, status de entrega, cliques em botão e mudanças de presença. Mas a maioria das APIs trata webhook como "POST e esquece": se cair, você perde o evento; se um atacante descobrir a URL, ele pode forjar mensagens; se sua API ficar 30s lenta, o provider desiste.
Na ZAP-API a plataforma de webhooks foi reconstruída em 2026 com 3 garantias que diferenciam um webhook "amador" de um webhook de produção: assinatura HMAC-SHA256, retry com backoff exponencial e Dead Letter Queue (DLQ) com replay one-click.
Por que isso importa
Um webhook sem essas três proteções é uma mina terrestre técnica:
- Sem assinatura: qualquer pessoa que descobrir a URL pode disparar eventos forjados — cobranças falsas, mensagens fantasmas, scripts maliciosos.
- Sem retry: se sua aplicação cair por 5 minutos durante um deploy, você perde todas as mensagens recebidas naquela janela. Para sempre.
- Sem DLQ: quando um payload específico falhar (ex: bug no seu parser), você não tem como reprocessar depois de corrigir o código.
1. Assinatura HMAC-SHA256 — provando que veio da ZAP-API
Toda chamada de webhook envia o header X-ZapAPI-Signature-256 no formato sha256=<hex>: o HMAC-SHA256 do corpo bruto, calculado com o seu webhook secret. O secret é gerado na primeira vez que você configura a URL (PUT /v1/instances/:id/webhook), ou quando você envia "secret": true para trocá-lo, e aparece uma única vez na resposta — guarde-o. Você também pode enviar a sua própria chave (16 a 128 caracteres).
// Node.js / Express
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.ZAPAPI_WEBHOOK_SECRET;
// IMPORTANTE: use raw body para o HMAC bater
app.post(
'/webhook/zap',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-zapapi-signature-256'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
// timingSafeEqual evita timing attack (e exige o mesmo tamanho)
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).json({ error: 'invalid_signature' });
}
const event = JSON.parse(req.body.toString());
console.log('Evento autenticado:', event.event, event.instanceId);
// Responda 2xx em até 5s — erro ou demora entra em retry
res.status(200).json({ ok: true });
}
);
app.listen(3000);
Regra de ouro: nunca confie em payload de webhook sem validar a assinatura. Mesmo que a URL pareça "secreta", basta um log mal redactado, um header vazado em screenshot, ou um proxy mal configurado para que a URL seja descoberta.
2. Retry com exponential backoff + jitter
Quando sua aplicação responde com erro (5xx, 403, 404, 429…), não responde em 5 segundos ou redireciona, a ZAP-API reagenda a entrega. São 6 tentativas: a primeira imediata e as seguintes depois de 1min, 5min, 15min, 1h, 6h — cerca de 7 horas no total, com uma variação aleatória de até 20% em cada espera.
O jitter (variação aleatória) evita o thundering herd — se 1.000 webhooks falharem ao mesmo tempo (típico em outage parcial), o retry não bate todos juntos no mesmo segundo.
Por que espaçar em horas? Porque a causa mais comum de falha é o seu servidor estar fora do ar — um deploy que deu errado, um certificado vencido, um túnel de desenvolvimento que caiu. Tentativas em segundos acabariam antes de você perceber; espalhadas por 7 horas, dão tempo de consertar sem perder o evento.
Três respostas encerram na hora, sem retry: 400 (o payload não serve para o seu endpoint), 401 (seu endpoint recusou a autenticação) e 410 (você sinalizou que o endpoint não existe mais — nesse caso a URL é removida da instância).
3. Dead Letter Queue (DLQ) — segunda chance, no seu tempo
Depois das 6 tentativas falhas, a entrega fica guardada como "morta" — até 90 dias contados da criação do evento (entregas bem-sucedidas ficam 30 dias no histórico). Você pode reenviá-la quando quiser — útil quando:
- Você acabou de fazer deploy do fix e quer reprocessar o que falhou.
- Um cliente reclamou que "não recebeu a confirmação de pedido" — você acha a entrega, reenvia, problema resolvido sem ressincronizar do zero.
- Você trocou a URL do webhook: o reenvio vai para a URL configurada agora.
Consultando e reenviando via API
// Histórico de entregas da instância (mais recentes primeiro)
GET /v1/instances/SEU_INSTANCE_ID/webhook/logs?limit=50&offset=0
Authorization: Bearer tk_seu_token
// Resposta
{
"logs": [
{
"id": 48213,
"event": "message.received",
"url": "https://meu-app.com/webhook",
"statusCode": 500,
"errorMessage": "Request failed with status code 500",
"delivered": false,
"attempt": 6,
"maxAttempts": 6,
"nextRetryAt": null,
"createdAt": "2026-04-30T12:34:56Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
// Reenviar uma entrega que esgotou as tentativas (ou falhou)
POST /v1/instances/SEU_INSTANCE_ID/webhook/deliveries/48213/replay
Authorization: Bearer tk_seu_token
O reenvio reaproveita a mesma entrega e o mesmo X-Zap-Event-Id, e recomeça as tentativas. Respostas possíveis: 409 WEBHOOK_PAUSED (reative o webhook no painel primeiro), 409 NO_WEBHOOK_URL (configure uma URL — acontece depois de um 410) e 422 para entrega já entregue ou com menos de 5 minutos e sem nova tentativa agendada.
Padrões de implementação que evitam dor de cabeça
Idempotência: cada evento traz um X-Zap-Event-Id único
Mesmo com retry, a ZAP-API pode entregar o mesmo evento mais de uma vez (ex: sua aplicação respondeu 200 mas a conexão caiu antes). O header X-Zap-Event-Id é o mesmo em todas as tentativas e no reenvio. Salve-o e ignore duplicatas:
const eventId = req.headers['x-zap-event-id'];
const seen = await db.events.findOne({ where: { eventId } });
if (seen) {
return res.status(200).json({ ok: true, duplicate: true });
}
await db.events.create({ eventId, payload: event });
// ...processar...
Acknowledge primeiro, processe depois
Não rode lógica pesada antes de retornar 200. Coloque o evento em uma fila interna (BullMQ, Redis Streams, SQS) e responda imediatamente:
app.post('/webhook/zap', async (req, res) => {
// 1. Valida HMAC (rápido)
if (!validSignature(req)) return res.status(401).end();
// 2. Enfileira (rápido)
await queue.add('process-webhook', JSON.parse(req.body));
// 3. Responde (rápido)
res.status(200).json({ ok: true });
});
Esse padrão derruba seu p99 de webhook de "1500ms quando o banco está lento" para "8ms sempre".
Eventos disponíveis
A ZAP-API entrega 21 tipos de evento via webhook:
message.received,message.sent,message.status,message.read,message.reaction,message.edited,message.deletedinstance.connected,instance.disconnected,instance.qrcode,instance.testgroup.join,group.leave,group.updatecontact.update,presence.updatecall.received,call.rejectedstatus.viewed,label.update,newsletter.update
Você escolhe quais quer receber em cada instância, no painel ou via PUT /v1/instances/:id/webhook.
FAQ
- Posso ter mais de uma URL de webhook por instância?
Não — cada instância tem uma URL. Para mandar eventos a vários sistemas, aponte para um endpoint seu que faz o fan-out. Você pode filtrar quais eventos recebe. - O HMAC é obrigatório?
O header é sempre enviado. Validar do seu lado é opcional, mas fortemente recomendado em produção. - O que acontece se eu não habilitar webhook?
Você pode usar polling (GET /v1/instances/:id/messages), mas perde latência (5-30s) e gasta mais quota de API. - Há rate limit no webhook?
Não há rate limit de saída — entregamos tudo. O limite é só na sua aplicação aguentar receber. - Quanto tempo a DLQ retém entregas?
Entregas que não chegaram: até 90 dias, contados da criação do evento. Entregas bem-sucedidas: 30 dias. Depois disso são apagadas.
Criar conta e configurar webhooks com HMAC, retry e DLQ — trial 7 dias, sem cartão.