Webhooks
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" -H "Content-Type: application/json" \ -d '{"url":"https://sua-aplicacao.exemplo/zapgate-webhook"}' \ https://sua-instancia.exemplo/instance/{id}/webhooksevents é opcional — vazio ou ausente significa todos os eventos. O
segredo (secret) usado para assinar as entregas só aparece nesta resposta
de criação; a listagem (GET .../webhooks) nunca devolve segredo (confirmado
ao vivo).
Verificando a assinatura
Seção intitulada “Verificando a assinatura”Toda entrega chega com o header X-Webhook-Signature, no formato:
t=<unix-timestamp>,v1=<hmac-sha256-hex>O HMAC cobre "<timestamp>.<corpo-bruto>" — ou seja, concatene o timestamp (em
texto), um . e o corpo exato dos bytes recebidos, e calcule
HMAC-SHA256(secret, timestamp + "." + body). Compare o hex resultante com o
v1 do header. Use o timestamp para rejeitar entregas fora de uma janela
aceitável (proteção contra replay).
Exemplo em pseudocódigo:
t, v1 = header.split(",") # "t=...", "v1=..."ts = t.removeprefix("t=")sig = v1.removeprefix("v1=")expected = hmac_sha256(secret, f"{ts}.{raw_body}").hexdigest()assert hmac.compare_digest(sig, expected)Testar sem esperar um evento real
Seção intitulada “Testar sem esperar um evento real”curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \ https://sua-instancia.exemplo/instance/{id}/webhooks/{webhookId}/testDispara um evento sintético webhook.test, assinado e entregue como qualquer
outro — ignora o filtro de events do webhook (sempre dispara, mesmo que
webhook.test não esteja na lista).
Log de entregas
Seção intitulada “Log de entregas”curl -H "Authorization: Bearer <ADMIN_TOKEN>" \ https://sua-instancia.exemplo/instance/{id}/webhooks/{webhookId}/deliveriesÚltimas 50 entregas. Shape real (confirmado ao vivo):
[ { "id": 1299, "eventType": "webhook.test", "status": "pending", "attempts": 0, "nextAttemptAt": "..." }, { "id": 1298, "eventType": "message.receipt", "status": "delivered", "attempts": 1, "lastCode": 200, "createdAt": "..." }]Eventos reais observados em produção: message.receipt e connection.update,
além do webhook.test sintético.
Se uma entrega falhar, ela fica pending/failed com lastError preenchido
(ex.: connection refused) e tenta de novo com backoff exponencial. Para
reenfileirar manualmente uma entrega failed/dead (zera as tentativas):
curl -X POST -H "Authorization: Bearer <ADMIN_TOKEN>" \ https://sua-instancia.exemplo/instance/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/retrySSRF: destinos privados são bloqueados por padrão
Seção intitulada “SSRF: destinos privados são bloqueados por padrão”O dispatcher resolve o host do webhook uma vez e conecta no IP exato
resolvido (evita reresolução DNS maliciosa entre a checagem e a entrega). Por
padrão, IP privado/loopback é recusado — só é permitido com
WEBHOOK_ALLOW_PRIVATE=true no backend, pensado para desenvolvimento local (o
próprio ambiente de testes do zapgate usa um receptor interno,
webhooksink, só alcançável porque essa flag está ligada ali).