Webhooks
Receba mensagens, status de entrega, conexão dos números e mais no seu endpoint, com assinatura HMAC, novas tentativas e proteção contra SSRF.
Com webhooks, o Zynzap faz um POST com JSON no seu endpoint a cada evento escolhido. É a forma mais simples de reagir a mensagens recebidas: não precisa manter conexão aberta. Os eventos são os mesmos do stream /events.
Configurar
Webhooks pertencem a uma chave de API. No painel, abra API e integrações, clique na chave e use a seção Webhooks:
- Até 5 endereços por chave, cada um com os seus eventos.
- Só aparecem os eventos que a chave já poderia ler em
/events: exigeevents:read, respeita os números da chave, e eventos da conta inteira (campanhas e aquecimento) só valem para chaves sem restrição de números. Se a chave for revogada, vencer ou perder a permissão, as entregas param na hora. - Segredo de assinatura (
whsec_...): aparece uma única vez ao criar. Novo segredo troca o segredo com carência opcional de 24 horas (as entregas levam as duas assinaturas até o fim dela). - Enviar teste manda um evento
webhook.ping(nenhuma mensagem sai pelo WhatsApp). O Registro mostra status HTTP, tempo, tentativas e motivo das falhas das últimas entregas. - Ativar/desativar a qualquer momento. Ao reativar, a entrega recomeça daquele momento em diante.
Requisitos do endereço
https://público, porta 443 ou 8443, sem usuário e senha na URL.- Endereços locais, de rede privada, loopback, link-local, CGNAT ou de metadados de nuvem são recusados no cadastro e em cada entrega: o domínio é resolvido de novo e fixado no endereço conferido.
- Redirecionamentos não são seguidos (um
3xxconta como falha). Não use proxy de redirecionamento.
Para desenvolver localmente, exponha sua máquina com um túnel HTTPS (por exemplo, cloudflared ou ngrok).
O que chega
POST /zynzap HTTP/1.1
Content-Type: application/json
User-Agent: Zynzap-Webhooks/1
X-Zynzap-Event-Id: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f
X-Zynzap-Event-Type: MessageReceived
X-Zynzap-Delivery-Id: 0b9e8d7c-6b5a-4938-8271-6a5b4c3d2e1f
X-Zynzap-Attempt: 1
X-Zynzap-Timestamp: 1760280000
X-Zynzap-Signature: v1=5d41402abc4b2a76b9719d911017c592...
{"id":"6f1c2d3e-...","type":"MessageReceived","created_at":"2026-10-12T14:00:00.000Z","cursor":"1042","session_id":"8c1f7a52-...","webhook_id":"4b3a2918-...","data":{"MessageReceived":{...}}}| Cabeçalho | Significado |
|---|---|
X-Zynzap-Event-Id | Mesmo valor de id. Use para ignorar repetições. |
X-Zynzap-Event-Type | Tipo do evento (ou webhook.ping). |
X-Zynzap-Delivery-Id | Identificador desta entrega (aparece no Registro do painel). |
X-Zynzap-Attempt | Número da tentativa, a partir de 1. |
X-Zynzap-Timestamp | Momento da assinatura, em segundos Unix. |
X-Zynzap-Signature | v1=<hex>; durante a carência de rotação, v1=...,v1=.... |
| Campo do corpo | Significado |
|---|---|
id | Identificador único do evento (idempotência). |
type | Tipo do evento. |
created_at | Quando o evento aconteceu (ISO 8601, UTC). |
cursor | Posição no histórico. Ordene por ele e use em /events?after_cursor= ou ReplayEvents para buscar o que faltou. |
session_id | Número de origem (null em eventos da conta inteira). |
webhook_id | Endereço que recebeu a entrega. |
data | { "<Tipo>": { ...campos } }, o mesmo JSON dos eventos domain de /events. |
Verificar a assinatura
A assinatura é HMAC-SHA256(segredo, "{X-Zynzap-Timestamp}.{corpo bruto}") em hexadecimal, com o prefixo v1=. Regras:
- Calcule sobre o corpo bruto, antes de interpretar o JSON (reformatar o JSON muda a assinatura).
- Compare em tempo constante.
- Recuse entregas com mais de 5 minutos (proteção contra reenvio).
- Aceite se qualquer
v1=conferir (carência de rotação).
import crypto from "node:crypto";
import http from "node:http";
// rawBody: corpo BRUTO (string), antes de JSON.parse.
export function verifyZynzap(rawBody, headers, secret = process.env.ZYNZAP_WEBHOOK_SECRET) {
const timestamp = headers["x-zynzap-timestamp"] ?? "";
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return (headers["x-zynzap-signature"] ?? "").split(",").some((part) => {
const [scheme, value = ""] = part.trim().split("=");
return (
scheme === "v1" &&
value.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected))
);
});
}
http
.createServer((req, res) => {
let rawBody = "";
req.setEncoding("utf8");
req.on("data", (chunk) => (rawBody += chunk));
req.on("end", () => {
if (!verifyZynzap(rawBody, req.headers)) {
res.writeHead(401).end();
return;
}
// Responda rápido e processe numa fila.
res.writeHead(204).end();
const event = JSON.parse(rawBody);
console.log(event.type, event.id);
});
})
.listen(8080);Guarde o segredo como uma senha (variável de ambiente ou cofre). Se ele vazar, gere um Novo segredo. Se só o endereço vazar, a assinatura impede que terceiros se passem pelo Zynzap.
Responder, novas tentativas e desativação
- Responda com qualquer 2xx em até 10 segundos (5 s para conectar) e processe depois, numa fila. O corpo da resposta é ignorado (lido até 16 KiB).
- Respostas fora de 2xx, redirecionamentos, tempo esgotado ou falha de conexão contam como falha. O Zynzap tenta de novo após 30 s, 2 min, 10 min, 30 min, 1 h, 2 h e 4 h: 8 tentativas no total, cerca de 8 horas.
- Depois de 15 falhas seguidas (em qualquer entrega), o endereço é desativado, a linha do tempo da chave registra o motivo e ele aparece como Desativado por falhas. Corrija e reative no painel; a entrega recomeça a partir daquele momento (use
cursoreReplayEventspara buscar o que perdeu). - A entrega é pelo menos uma vez: o mesmo
idpode chegar mais de uma vez e a ordem não é garantida. Deduplique poride ordene porcursor.
Testar
- Cadastre o endereço e clique em Enviar teste: chega um
webhook.pingassinado. - Confira o Registro (status, tempo, motivo da falha).
- Para testar sem o painel, gere a assinatura você mesmo (aba cURL acima).
- Para eventos reais, envie uma mensagem de outro celular para o seu número: chega
MessageReceived.
{
"id": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"type": "webhook.ping",
"created_at": "2026-10-12T14:00:00.000Z",
"cursor": null,
"session_id": null,
"webhook_id": "4b3a2918-0f1e-4d2c-9b8a-7c6d5e4f3a2b",
"data": { "message": "Evento de teste do Zynzap. Nenhuma mensagem foi enviada." }
}Tipos de evento
Lista gerada do catálogo do Zynzap, com um exemplo de corpo para cada tipo (valores fictícios).
Mensagens
Números
Grupos e presença
Atendimento e chamadas
Campanhas e aquecimento
Webhook ou stream?
Os dois entregam os mesmos eventos. O webhook é melhor para sistemas que só recebem requisições (n8n, CRMs, funções serverless). O stream /events é bom para um processo que já fica conectado e quer latência mínima. Dá para usar os dois com a mesma chave.