É 2h da manhã. Cliente liga: "as confirmações de pedido pararam de chegar no WhatsApp dos meus clientes". Você abre o terminal, faz tail -f nos logs do servidor, procura "webhook" no meio do barulho, encontra alguns 500 misturados com 200 — mas qual deu errado? Para qual cliente? Foi nosso bug ou bug do servidor dele?
Para isso a ZAP-API guarda o histórico de cada entrega de webhook da sua instância: para onde foi, qual evento, o que o seu servidor respondeu, em que tentativa está e quando sai a próxima. E o que falhou pode ser reenviado pela API, com o token da instância, ou pelo painel.
O que fica registrado em cada entrega
- ID da entrega (número, ex.:
48213) — é ele que você usa para reenviar - URL de destino no momento da entrega
- Tipo do evento (message.received, message.status, instance.connected…)
- Status HTTP que o seu servidor respondeu, ou vazio quando não houve resposta
- Mensagem de erro da última tentativa que falhou (ex.:
Request failed with status code 500,timeout of 5000ms exceeded) — pode continuar preenchida mesmo depois de uma tentativa bem-sucedida - Tentativa atual e o máximo (6)
- Próxima tentativa agendada, ou vazio quando não há mais
Como funciona o retry
Cada evento tem até 6 tentativas: a primeira imediata e as seguintes depois de 1min, 5min, 15min, 1h, 6h — cerca de 7 horas no total, com variação aleatória de até 20% em cada espera. Conta como falha: resposta de erro (5xx, 403, 404, 429…), redirecionamento ou nenhuma resposta em 5 segundos.
Três respostas encerram na hora, sem retry: 400, 401 e 410. O 410 também remove a URL da instância, porque é o seu servidor dizendo que o endpoint não existe mais.
Esgotadas as tentativas, a entrega fica guardada para você reenviar — até 90 dias contados da criação do evento. Entregas bem-sucedidas ficam 30 dias no histórico.
Tentativas e reenvios podem chegar fora de ordem em relação a eventos mais novos, e o timestamp do corpo é sempre o do evento original. Ordene pelo timestamp e não rejeite um evento só por ser antigo.
Consultando o histórico via API
curl "https://api.zap-api.tech/v1/instances/SEU_INSTANCE_ID/webhook/logs?limit=50&offset=0" \
-H "Authorization: Bearer tk_seu_token"
Resposta (mais recentes primeiro):
{
"logs": [
{
"id": 48213,
"event": "message.received",
"url": "https://meusite.com.br/webhooks/zap",
"statusCode": 500,
"errorMessage": "Request failed with status code 500",
"delivered": false,
"attempt": 3,
"maxAttempts": 6,
"nextRetryAt": "2026-05-15T03:52:00Z",
"createdAt": "2026-05-15T03:30:11Z"
}
],
"total": 47,
"limit": 50,
"offset": 0
}
O limit vai até 100. Para um resumo do período (total, entregues, falhas, taxa de sucesso e a última entrega), use GET /v1/instances/SEU_INSTANCE_ID/webhook/stats?days=7 (máximo 30).
Reenviando uma entrega
Depois de corrigir o problema no seu servidor, reenvie o que falhou:
curl -X POST "https://api.zap-api.tech/v1/instances/SEU_INSTANCE_ID/webhook/deliveries/48213/replay" \
-H "Authorization: Bearer tk_seu_token"
- O reenvio aceita entregas que não chegaram e não estão em andamento. Entrega já entregue, ou com menos de 5 minutos e sem nova tentativa agendada, responde
422. - O reenvio reaproveita a mesma entrega e o mesmo
X-Zap-Event-Id— o seu deduplicador reconhece o evento — e recomeça as tentativas. - Se você trocou a URL do webhook, o reenvio vai para a URL configurada agora.
- Webhook pausado:
409 WEBHOOK_PAUSED— reative-o no painel primeiro. Instância sem URL (por exemplo, depois de um410):409 NO_WEBHOOK_URL. - Só entregas da própria instância do token: id de outra instância responde
404.
Script de reenvio em lote
Cenário comum: seu servidor ficou fora do ar por algumas horas e parte das entregas esgotou as tentativas. Este script percorre o histórico e reenvia as que não chegaram:
// reenviar-falhas.js
const axios = require("axios");
const INSTANCE = process.env.ZAP_INSTANCE_ID;
const ZAP = axios.create({
baseURL: "https://api.zap-api.tech/v1/instances/" + INSTANCE,
headers: { Authorization: "Bearer " + process.env.ZAP_TOKEN },
});
async function reenviarFalhas(desdeIso) {
const desde = new Date(desdeIso).getTime();
let offset = 0;
let total = 0;
while (true) {
const { data } = await ZAP.get("/webhook/logs", { params: { limit: 100, offset } });
if (data.logs.length === 0) break;
for (const e of data.logs) {
if (new Date(e.createdAt).getTime() < desde) return console.log("Reenviadas: " + total);
// não chegou, nada agendado e com mais de 5 min (as mais novas ainda podem estar saindo)
const esgotou = !e.delivered && e.nextRetryAt === null
&& Date.now() - new Date(e.createdAt).getTime() > 5 * 60 * 1000;
if (!esgotou) continue;
try {
await ZAP.post("/webhook/deliveries/" + e.id + "/replay");
total++;
} catch (err) {
const status = err.response ? err.response.status : null;
if (status === 409) return console.error("Webhook pausado ou sem URL: resolva no painel e rode de novo.");
console.error("erro " + e.id + ": " + (status || err.message));
}
await new Promise((r) => setTimeout(r, 200)); // não estoure o limite da API
}
offset += data.logs.length;
}
console.log("Reenviadas: " + total);
}
reenviarFalhas("2026-05-15T00:00:00Z");
Validação da assinatura no seu endpoint
Toda entrega leva o header X-ZapAPI-Signature-256 no formato sha256=<hex>, calculado sobre o corpo bruto com o secret do webhook. O secret é gerado na primeira configuração da URL (ou quando você envia "secret": true para trocá-lo) e aparece uma única vez na resposta; você também pode definir o seu. Valide a assinatura para garantir que a requisição veio da ZAP-API:
const crypto = require("crypto");
const express = require("express");
const app = express();
const SECRET = process.env.ZAP_WEBHOOK_SECRET;
// IMPORTANTE: corpo bruto antes do JSON parser
app.post("/webhooks/zap", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-zapapi-signature-256"] || "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("invalid signature");
}
const evento = JSON.parse(req.body.toString()); // { event, instanceId, timestamp, data }
res.status(200).send("ok"); // responda rápido; processe depois
});
Outros headers de cada entrega: X-Zap-Event-Id (igual em todas as tentativas e no reenvio — use para deduplicar), X-Zap-Instance e X-Zap-Timestamp. Quem configurou um secret recebe também o header antigo X-Zap-Signature, mantido por compatibilidade.
Casos práticos
Caso 1: bug que só apareceu em produção
Algumas confirmações de pedido não chegam. No histórico você vê entregas de message.status com statusCode: 500, descobre no seu log o erro num campo opcional, corrige e reenvia as que esgotaram as tentativas. Se o conserto saiu em menos de 7 horas, a maioria nem precisou de reenvio: o próprio retry entregou.
Caso 2: mudança de URL do webhook
O time migra o backend de servidor e a URL muda. Atualize a URL no painel ou com PUT /v1/instances/:id/webhook. As entregas novas vão para a URL nova. As que falharam na URL antiga param de ser re-tentadas automaticamente; reenvie-as manualmente e elas vão para a URL nova.
FAQ
Quanto tempo as entregas ficam guardadas?
Entregas bem-sucedidas: 30 dias. Entregas que não chegaram: 90 dias. Depois disso são apagadas. Para guardar por mais tempo, grave os eventos do seu lado ao recebê-los.
O reenvio duplica o evento?
O evento chega de novo ao seu endpoint, com o mesmo X-Zap-Event-Id. Desenhe seu handler para ser idempotente: se já processou aquele id, responda 200 e ignore.
Diferença entre 5xx e timeout?
5xx é o seu servidor respondendo com erro. Timeout é não responder em 5 segundos. Os dois contam como falha e disparam retry. No histórico, o timeout aparece sem statusCode e com a mensagem de erro do tempo esgotado.
E se o meu endpoint falhar por muito tempo?
Se, numa janela de 6 horas, o endpoint não tiver nenhum sucesso e acumular pelo menos 50 respostas 3xx, 400, 401, 403, 404, 410 ou 429, ou erros de conexão por mais de uma hora, o webhook da instância é pausado e você recebe um aviso por e-mail. Erros 5xx não pausam (eles seguem o retry normal). Durante a pausa guardamos até 5.000 eventos por instância — os seguintes são descartados. Para despausar, reative no painel ou configure uma URL nova; depois reenvie o que ficou guardado.
Próximo passo
Criar conta grátis e teste a integração com histórico e reenvio desde o primeiro dia.