▸ API · Webhooks
Eventos
scan.completed, scan.failed e finding.opened — quando saem e o que trazem.
atualizado em 2 set 2026versão v1
Três eventos, nomeados no padrão recurso.aconteceu. Todos chegam no mesmo envelope; o que muda é o data.
| Evento | Quando sai | |
|---|---|---|
scan.completed | Uma ronda terminou | detalhes ↓ |
scan.failed | Uma ronda falhou | detalhes ↓ |
finding.opened | O plantão encontrou brecha nova | detalhes ↓ |
scan.completed — Uma ronda terminou
Toda ronda que passa pela fila e é gravada como done — disparada no painel, por POST /scans ou pelas integrações.
As rondas que rodam por dentro, sem passar pela fila, NÃO emitem este evento: o plantão agendado (que tem o
finding.opened) e o scan-on-deploy.O relatório completo não vem no webhook. Use
scan_id em GET /scans/{id}/report — assim a carga fica pequena e o conteúdo sensível sai por uma porta autenticada por você.Campos de data
| Campo | Descrição |
|---|---|
scan_idstringsempre | Id da ronda. |
hostnamestringsempre | Alvo normalizado. |
targetstringsempre | URL escaneada. |
risk_scoreintegersempre | Exposição (maior = pior). |
risk_labelstringsempre | Faixa do score.SECURELOW_RISKMODERATE_RISKHIGH_RISKCRITICAL_RISK |
critical_countintegersempre | Achados critical. |
blockedbooleansempre | A ronda não viu o app. Trate como inconclusivo, não como "seguro". |
report_urlstring | Link do relatório no painel. |
Exemplo
{
"id": "evt_9f1c2a54-4c1e-4a0c-9a1f-7b6a2f0d8e33",
"event": "scan.completed",
"createdAt": "2026-08-29T14:22:07.481Z",
"data": {
"scan_id": "scan_5e2a91c4",
"hostname": "app.exemplo.com.br",
"target": "https://app.exemplo.com.br",
"risk_score": 41,
"risk_label": "MODERATE_RISK",
"critical_count": 1,
"blocked": false,
"report_url": "https://www.guarita.dev/scans/scan_5e2a91c4"
}
}scan.failed — Uma ronda falhou
A ronda estourou o tempo máximo sem produzir resultado, ou a fila desistiu depois de todas as tentativas.
Assine junto com
scan.completed: só um dos dois chega pra cada ronda. Tratar só o sucesso faz "ronda que não rodou" virar silêncio — indistinguível de "não encontrei nada".Campos de data
| Campo | Descrição |
|---|---|
scan_idstringsempre | Id da ronda. |
hostnamestringsempre | Alvo normalizado. |
targetstringsempre | URL que seria escaneada. |
reasonstringsempre | A causa, em português. |
Exemplo
{
"id": "evt_0b7e4d21-8a3f-4c6e-b2d1-5f9a0c3e7d18",
"event": "scan.failed",
"createdAt": "2026-08-29T15:01:33.902Z",
"data": {
"scan_id": "scan_c3d9e0a1",
"hostname": "app.exemplo.com.br",
"target": "https://app.exemplo.com.br",
"reason": "A ronda excedeu o tempo máximo e não produziu resultado."
}
}finding.opened — O plantão encontrou brecha nova
Uma ronda de plantão (agendada) abriu ao menos um achado que não existia na ronda anterior do mesmo app. Ronda limpa não gera evento.
Não sai de ronda inconclusiva (bloqueada ou parcial por tempo): comparar um retrato incompleto com um completo inventaria "achados novos". Por isso
blocked e incomplete vêm sempre false.Na PRIMEIRA ronda de um app não há baseline: todos os achados contam como novos e
verdict vem clean (a ronda estabelece a base). Se o seu tratamento abre chamado por item, considere ignorar eventos com previous_risk_score: null.É independente do limiar de alerta por e-mail: o limiar é preferência de quem lê a caixa de entrada, não contrato de integração.
Campos de data
| Campo | Descrição |
|---|---|
scan_idstringsempre | Id da ronda de plantão. |
hostnamestringsempre | O app. |
verdictstringsempre | breach = achado novo grave, ou salto de 15+ pontos · warning = piorou, sem gravidade · clean = primeira ronda (base).breachwarningclean |
risk_scoreintegersempre | Exposição agora. |
previous_risk_scoreinteger | nullsempre | Exposição da ronda anterior. null quando não havia. |
openedstring[]sempre | TÍTULOS dos achados novos (não ids — o título é a chave de identidade entre rondas). |
resolvedstring[]sempre | Títulos dos achados que sumiram desde a anterior. |
blockedbooleansempre | Sempre false (ronda bloqueada não emite). |
incompletebooleansempre | Sempre false (ronda parcial não emite). |
Exemplo
{
"id": "evt_2c7d0b18-6f4a-4b7e-9c31-0a5d8e12b774",
"event": "finding.opened",
"createdAt": "2026-08-29T03:11:52.094Z",
"data": {
"scan_id": "scan_a17b3f90",
"hostname": "app.exemplo.com.br",
"verdict": "breach",
"risk_score": 58,
"previous_risk_score": 41,
"opened": [
"Chave service_role do Supabase exposta no bundle",
"Content-Security-Policy ausente"
],
"resolved": [
"Strict-Transport-Security (HSTS) ausente"
],
"blocked": false,
"incomplete": false
}
}Achou algo errado ou faltando? help@guarita.dev — com o
x-request-id, se for sobre uma resposta.