# Guarita API > API pública da Guarita (guarita.dev): dispare rondas de segurança nos seus apps, leia relatórios e receba webhooks assinados. PT-BR. Atualizada em 2026-09-02. Base URL: https://api.guarita.dev/v1/public Autenticação: header `Authorization: Bearer gsk_...` (chave criada em Configurações → Desenvolvedores; planos Pro e Business). Formato: JSON (exceto GET /scans/{id}/report.pdf, que devolve application/pdf). Datas em ISO 8601 UTC. Campos em snake_case (exceto `summary` e o relatório, que são o formato do scanner, em camelCase). Toda resposta traz o header `x-request-id` — cite-o ao falar com o suporte (help@guarita.dev). OpenAPI 3.1: https://www.guarita.dev/docs/api/openapi.json ## Escopos - scans:read — ler rondas e relatórios (GET /me, GET /scans, GET /scans/{id}, GET /scans/{id}/report, GET /scans/{id}/report.pdf) - scans:write — disparar e cancelar rondas (POST /scans, POST /scans/{id}/cancel) - apps:read — listar apps (GET /apps) Chave: prefixo `gsk_`, 256 bits, mostrada UMA vez na criação; até 10 chaves ativas por conta; revogar vale na hora. Chave com scans:write exige, na criação, a declaração de que só escaneia apps próprios ou autorizados (gravada com data, versão dos Termos, IP e user-agent). Por isso POST /scans não pede `authorized` no corpo. A única confirmação por ronda é `authorized_intrusive: true`, e só com `aggressive: true`. 401 = credencial (chave ausente/inválida/revogada ou plano sem API). 403 insufficient_scope = chave válida sem o escopo (campo `required`). Nunca repita um 401/403. ## Endpoints ### GET /me — Confirma que a chave funciona (escopo: scans:read) Ping autenticado: devolve a conta dona da chave, os escopos que ela carrega e o plano em vigor. É o primeiro request de toda integração nova — e o primeiro a rodar quando algo para de funcionar. Repare que exige `scans:read`: uma chave criada só com `apps:read` recebe `403` aqui, mesmo sendo válida. Nesse caso, teste com `GET /apps`. Resposta 200 — A chave é válida. ```json { "account_id": "acc_9f2c1b7e", "key_id": "key_3a8d41c6", "scopes": [ "scans:read", "scans:write", "apps:read" ], "plan": "pro" } ``` Erros possíveis: unauthorized, insufficient_scope, bad_request, internal ### GET /apps — Lista os apps monitorados (escopo: apps:read) Os apps da conta. Um app entra aqui de duas formas: cadastrado no painel (nasce com `last_scan_at: null`) ou automaticamente na primeira ronda contra o hostname — pelo painel ou por `POST /scans`. Nos dois casos ocupa uma vaga do limite do plano. A lista não é paginada: o número de apps é limitado pelo plano (Pro: 10 · Business: sem limite), então cabe numa resposta. Resposta 200 — Apps da conta. ```json { "data": [ { "hostname": "app.exemplo.com.br", "created_at": "2026-05-14T09:21:44.108Z", "last_scan_at": "2026-08-27T14:09:24.106Z", "verified": true }, { "hostname": "staging.exemplo.com.br", "created_at": "2026-07-02T18:40:02.771Z", "last_scan_at": null, "verified": false } ] } ``` Erros possíveis: unauthorized, insufficient_scope, bad_request, internal Nota: Projetos protegidos pela integração da Vercel ou do Lovable NÃO aparecem: eles ocupam a vaga do plano por um registro interno que nunca é alvo de rede. A ronda deles sai pela tela da integração — `POST /scans` contra o hostname de um projeto atestado responde `409 domain_provider_conflict`. Nota: `verified` diz que a propriedade JÁ FOI comprovada alguma vez. Uma atestação de provedor (Cloudflare) pode vencer ou ser revogada e o app seguir `true` aqui. Trate `403 domain_unverified` no `POST /scans` de qualquer forma. ### GET /scans — Histórico de rondas (escopo: scans:read) As rondas da conta, da mais recente pra mais antiga, paginadas por cursor. `risk_score` + `critical_count` são o par que uma automação usa pra decidir se derruba o build; `risk_label` vai junto porque o número sozinho não é acionável. O histórico respeita a retenção do plano (Pro: 365 dias · Business: sem limite). Rondas mais antigas não estão "na próxima página" — não existem mais. Query: - limit (integer, padrão 20): Rondas por página. Teto `50` (valores acima são reduzidos a 50; ausente, inválido ou ≤ 0 cai no padrão). - before (string): Cursor: só rondas criadas ANTES deste instante (exclusivo). Use o `next_before` da página anterior. Data não-ISO responde `400 invalid_cursor`. Resposta 200 — Uma página do histórico. ```json { "data": [ { "id": "scan_7b3e9a12", "hostname": "app.exemplo.com.br", "status": "done", "created_at": "2026-08-27T14:02:56.902Z", "finished_at": "2026-08-27T14:09:24.106Z", "risk_score": 78, "risk_label": "HIGH_RISK", "critical_count": 2, "blocked": false }, { "id": "scan_1c40de55", "hostname": "staging.exemplo.com.br", "status": "done", "created_at": "2026-08-21T11:15:03.517Z", "finished_at": "2026-08-21T11:19:42.204Z", "risk_score": 34, "risk_label": "MODERATE_RISK", "critical_count": 0, "blocked": false } ], "next_before": "2026-08-21T11:15:03.517Z" } ``` Erros possíveis: unauthorized, insufficient_scope, bad_request, internal, invalid_cursor Nota: `next_before` vem preenchido sempre que a página veio CHEIA — inclusive quando o total é múltiplo exato do `limit` (a próxima chamada devolve `data: []` e `next_before: null`). Pare o laço em `next_before === null`, não em `data.length < limit`. ### POST /scans — Dispara uma ronda (escopo: scans:write) Enfileira uma ronda e responde `202` na hora com o `id`. A ronda não é instantânea (busca o app, roda dezenas de verificações e, com IA, ainda analisa), então o resultado você busca depois em `GET /scans/{id}` — ou recebe pelo webhook `scan.completed`. Só `target` é obrigatório: sem mais nada, é a ronda padrão do painel (IA e infra ligadas, o resto desligado). As demais opções são as MESMAS da tela — testes autenticados, modo agressivo, LGPD e escopo extra de infra — com os mesmos padrões e as mesmas travas. Cota, limite de apps, plano, verificação e autorização são decididos pelo MESMO caso de uso do painel: a chave muda quem chama, nunca o que é permitido. Ordem das travas: uso justo (`429`) → lista de bloqueio (`403`) → recurso do plano (`403 feature_locked`) → propriedade do app (`403 domain_unverified`) → confirmação intrusiva (`400`) → cota de IA (`402`) → limite de apps (`402`). Corpo (JSON): - target (string, obrigatório): URL completa do alvo, começando com `http://` ou `https://`. - ai (boolean, padrão true): Análise com IA: explica cada achado e entrega o conserto pronto. Consome 1 análise da cota do mês e exige app verificado. - infra (boolean, padrão true): Descoberta e sondagem da infraestrutura por trás do app (Supabase, Firebase, clouds). Só leitura. - authenticated (boolean, padrão false): Testes autenticados: a Guarita cria um usuário de teste no seu app e checa, por dentro, se dá pra ver dados de outras pessoas. Exige plano com o recurso (Pro+) e app verificado. Pode disparar o e-mail de boas-vindas do seu app. - aggressive (boolean, padrão false): Modo agressivo: confirma as brechas explorando de verdade — cria, altera e apaga dados de teste (e limpa depois). Liga sozinho `authenticated` e `infra`. Exige plano com o recurso (Pro+), app verificado e `authorized_intrusive: true`. Prefira um ambiente de teste. - authorized_intrusive (boolean, padrão false): Só é lido com `aggressive: true`: a sua confirmação, POR RONDA, de que tem autorização expressa pra testes intrusivos neste alvo — os Termos (§2) exigem. Sem ela, `400 intrusive_unauthorized`. - lgpd (boolean, padrão false): Pré-diagnóstico LGPD: consentimento de cookies, rastreadores, política de privacidade, dado pessoal exposto e dados saindo do país. Exige plano com o recurso (Pro+). São sinais técnicos — não é parecer jurídico. - infra_scope (string[]): Outros endereços SEUS que o app usa (`api.`, `cdn.`…), pra entrarem na sondagem de infra. Array de hostnames ou uma string separada por vírgula. Só o que é seu ou que você tem permissão de testar. Exemplo de corpo: {"target":"https://app.exemplo.com.br","ai":true} Resposta 202 — Ronda aceita e enfileirada. ```json { "id": "scan_e11a03d9", "status": "running" } ``` Erros possíveis: unauthorized, insufficient_scope, bad_request, internal, invalid_target, intrusive_unauthorized, quota_exceeded, domain_limit, feature_locked, target_blocked, domain_unverified, domain_provider_conflict, rate_limited Nota: A declaração de que você só escaneia apps seus (ou com autorização expressa) mora na CHAVE: é a checkbox obrigatória ao criar uma chave com `scans:write`, gravada com data, versão dos Termos, IP e user-agent. Por isso o corpo desta rota não pede nenhuma confirmação de autorização — a única exceção é o modo agressivo, logo abaixo. Nota: `aggressive: true` mexe no app de verdade (cria, altera e apaga dados de teste) e por isso exige, ALÉM da declaração da chave, `authorized_intrusive: true` em cada ronda — sem ele, `400 intrusive_unauthorized`. Não embuta essa confirmação num wrapper genérico que qualquer job chama com qualquer URL: deixe-a perto de quem escolheu o alvo. Nota: `ai`, `authenticated` e `aggressive` exigem app com prova de propriedade VIGENTE; sem ela, `403 domain_unverified`. Trate como estado esperado: refaça com `ai: false` (ronda básica, sem cota, sem verificação), avise quem cuida da integração e deixe o build seguir. Nota: `authenticated`, `aggressive` e `lgpd` dependem de recurso do plano; sem ele, `403 feature_locked` com `feature` e `upgradeTo`. Hoje todo plano com API (Pro e Business) inclui os três, mas a trava é avaliada a cada chamada pelo plano daquele instante — trate o código no cliente. ### GET /scans/{id} — Estado e resumo de uma ronda (escopo: scans:read) O endpoint de polling depois de `POST /scans`. Enquanto a ronda roda, `progress` diz onde ela está (fase, verificações feitas/previstas, verificação atual) e `summary` é `null`; quando `status` vira `done`, `progress` vira `null` e `summary` traz score, faixa e contagem de críticos. Um id de outra conta responde `404`, igual a um id inexistente — a API não confirma a existência de rondas de terceiros. Parâmetros de rota: - id (string): Id da ronda (`scan_…`), devolvido por `POST /scans` ou por `GET /scans`. Resposta 200 — A ronda. ```json { "id": "scan_7b3e9a12", "hostname": "app.exemplo.com.br", "target": "https://app.exemplo.com.br", "status": "done", "created_at": "2026-08-27T14:02:56.902Z", "finished_at": "2026-08-27T14:09:24.106Z", "progress": null, "summary": { "id": "scan_7b3e9a12", "target": "https://app.exemplo.com.br", "hostname": "app.exemplo.com.br", "status": "done", "score": 78, "riskLabel": "HIGH_RISK", "criticalCount": 2, "finishedAt": "2026-08-27T14:09:24.106Z", "durationMs": 387096, "blocked": false } } ``` Erros possíveis: unauthorized, insufficient_scope, bad_request, internal, not_found Nota: Em `summary.blocked: true`, o número não representa o app — a Guarita não conseguiu enxergá-lo. Falhar o build por isso pune o time errado. Ronda bloqueada não consome cota de IA. ### POST /scans/{id}/cancel — Cancela uma ronda em andamento (escopo: scans:write) Interrompe uma ronda que ainda está rodando (`queued`, `running` ou `analyzing`) e responde com o estado dela. É o botão "Cancelar" da tela de espera, pra quando o pipeline foi abortado ou a ronda saiu por engano. Um id de outra conta responde `404`, igual a um id inexistente. Parâmetros de rota: - id (string): Id da ronda (`scan_…`), devolvido por `POST /scans` ou por `GET /scans`. Resposta 200 — O estado da ronda depois do pedido. ```json { "id": "scan_7b3e9a12", "status": "canceled", "finished_at": "2026-09-02T12:00:00.000Z" } ``` Erros possíveis: unauthorized, insufficient_scope, bad_request, internal, not_found Nota: Idempotente: ronda que já terminou (`done`, `failed` ou `canceled`) volta como está, com `200` — cancelar duas vezes não quebra nada. Leia `status` na resposta em vez de assumir `canceled`. Nota: Ronda cancelada não consome cota: a análise com IA reservada no disparo (`ai: true`) é liberada. O cancelamento é cooperativo — o estado vira `canceled` na hora e a ronda para na próxima etapa, sem sobrescrever esse estado com `done` ou `failed`. ### GET /scans/{id}/report — Relatório completo de uma ronda (escopo: scans:read) Achados, evidências, camada leiga, conserto pronto, análise da IA, inventário de infra e o que o app já acerta. É o mesmo documento que o painel mostra e o PDF imprime — passa pelo mesmo preparo: o que o seu plano não destrava na tela também não sai por aqui. O relatório só existe quando a ronda produziu resultado. Antes disso (e numa ronda que falhou sem gerar nada) a resposta é `404` — estado normal de uma ronda em andamento, não erro de integração. Consulte `GET /scans/{id}` pra saber quando pedir. Parâmetros de rota: - id (string): Id da ronda (`scan_…`), devolvido por `POST /scans` ou por `GET /scans`. Resposta 200 — O relatório (recortado — um relatório real tem dezenas de achados). ```json { "schemaVersion": "1.0", "scan": { "id": "scan_7b3e9a12", "target": "https://app.exemplo.com.br", "hostname": "app.exemplo.com.br", "stack": [ "Lovable", "Supabase", "Vercel" ], "status": "done", "startedAt": "2026-08-27T14:02:57.010Z", "finishedAt": "2026-08-27T14:09:24.106Z", "durationMs": 387096, "modulesRun": 37 }, "findings": [ { "id": "SECRETS-001", "module": "secrets", "severity": "critical", "cvss": 9.8, "title": "Supabase service_role key exposta no bundle JS", "description": "Chave de serviço (bypassa RLS) encontrada em index-CBWCsxTw.js. Concede acesso administrativo total à API de dados.", "evidence": "eyJhbGciOiJIUzI1Ni… · role: \"service_role\" · fonte: app.exemplo.com.br/assets/index-CBWCsxTw.js", "recommendation": "Rotacione a service_role e use apenas a anon key no client.", "confidence": "firm", "friendly": { "headline": "Sua \"senha mestra\" do banco está exposta no site", "whatItIs": "A service_role é a chave de administrador do seu Supabase. Ela está escrita no JavaScript que qualquer visitante baixa só de abrir seu site.", "whyItMatters": "Com ela, qualquer pessoa pode ler, alterar ou apagar todos os dados do seu app.", "urgency": "now" }, "fix": { "kind": "code", "targets": [ "lovable", "cursor", "v0", "bolt" ], "prompt": "Remova a chave service_role do Supabase de todo o código do front-end…", "code": "const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)" } }, { "id": "CORS-001", "module": "cors", "severity": "high", "cvss": 7.5, "title": "CORS reflete qualquer Origin com credenciais", "description": "A API devolve Access-Control-Allow-Origin igual ao Origin recebido, com credentials.", "evidence": "Origin: https://evil.test → Access-Control-Allow-Origin: https://evil.test", "recommendation": "Restrinja as origens permitidas ao seu domínio.", "confidence": "confirmed", "fixWithheld": true } ], "ai": { "executive_summary": "O app expõe uma credencial administrativa do banco e opera com RLS desligado…", "risk_score": { "score": 78, "label": "HIGH_RISK", "justification": "Credencial privilegiada exposta + RLS desabilitado permitem takeover total do banco." }, "risk_scenarios": [], "owasp_mapping": [ { "category": "A02:2021-Cryptographic Failures", "finding_ids": [ "SECRETS-001" ], "status": "vulnerable" } ], "remediation_plan": [ { "priority": 1, "finding_ids": [ "SECRETS-001" ], "action": "Esconder a chave de administrador do Supabase", "effort": "quick_win", "impact_if_not_fixed": "Acesso total ao banco por qualquer visitante." } ], "additional_insights": [], "infra_inventory": [ { "provider": "supabase", "asset": "plsvzxvydhkvlxbtxgrs.supabase.co", "kind": "baas_project", "exposure": "credential_leaked", "notes": "service_role exposta no bundle." } ], "cross_infra_chains": [], "discovered_vulnerabilities": [] }, "infra": { "assets": [], "credentials": [], "providers": [ "supabase", "gcp", "vercel" ], "scopeHosts": [ "app.exemplo.com.br", "plsvzxvydhkvlxbtxgrs.supabase.co" ], "notes": [] }, "strengths": [ "HTTPS ativo e certificado válido", "Nenhuma chave do Stripe exposta" ] } ``` Erros possíveis: unauthorized, insufficient_scope, bad_request, internal, not_found Nota: Escreva o consumidor tolerando `fix` ausente com `fixWithheld: true`. Hoje todo plano com API também destrava o conserto, mas o gate é avaliado a cada requisição a partir do plano naquele instante — uma integração que assume `fix` sempre presente quebra no dia em que o plano muda. ### GET /scans/{id}/report.pdf — Relatório de uma ronda em PDF (escopo: scans:read) O mesmo documento que o botão "Salvar PDF" do painel gera — pra anexar no chamado, arquivar no CI ou mandar pro cliente. Passa pelo MESMO preparo e pela mesma trava de plano do relatório em JSON: o que não sai na tela não sai aqui. No Business, vem com a marca da conta. Só existe quando a ronda produziu resultado; antes disso a resposta é `404`, como em `GET /scans/{id}/report`. Parâmetros de rota: - id (string): Id da ronda (`scan_…`), devolvido por `POST /scans` ou por `GET /scans`. Resposta 200 — O PDF, como anexo (`Content-Disposition: attachment`). Corpo: application/pdf (binário; salve em arquivo, não faça res.json()). Erros possíveis: unauthorized, insufficient_scope, bad_request, internal, not_found Nota: Nome sugerido no `Content-Disposition`: `guarita-.pdf` — ou `-.pdf` quando a conta tem marca própria (Business). Vem com `Cache-Control: no-store`: o conteúdo depende do plano e das marcações do momento, não guarde em cache intermediário. Nota: Achados marcados como alarme falso no painel ficam de fora do PDF — o anexo que você arquiva não contradiz a tela. O JSON de `GET /scans/{id}/report` continua trazendo todos. ## Erros Todo erro: `{ "error": "", "message": "", ...extras }`. Trate pelo `error`. - 400 bad_request: Corpo malformado (JSON inválido). O status acompanha o motivo: `413` com corpo acima de 1 MB, `415` com `Content-Type` que não seja JSON — o código `error` continua `bad_request`. → Corrija a requisição. (corrija e repita) - 400 invalid_target: `target` ausente ou não é URL completa (`http://` ou `https://`). → Mande a URL inteira do alvo. (corrija e repita) - 400 intrusive_unauthorized: `aggressive: true` sem `authorized_intrusive: true`. O modo agressivo mexe no app de verdade, e a confirmação é por ronda — a declaração da chave não basta. → Mande `authorized_intrusive: true` junto — só se você tem autorização expressa pra testes intrusivos neste alvo. Ou tire o `aggressive`. (corrija e repita) - 400 invalid_cursor: `before` não é uma data ISO 8601. → Use o `next_before` que veio na página anterior. (corrija e repita) - 401 unauthorized: Chave ausente, fora do formato `Bearer gsk_…`, inexistente, revogada — ou o plano da conta deixou de incluir a API. O corpo é o MESMO em todos os casos, de propósito. Vem com o header `WWW-Authenticate`. → Confira a variável de ambiente, o estado da chave no painel e o plano da conta (`GET /me` com outra chave). Repetir nunca resolve. (não repita) - 402 quota_exceeded: Cota de análises com IA do mês esgotada (só com `ai: true`). → Refaça com `ai: false` (ronda básica, sem cota) ou avise quem cuida da assinatura. Não é bug: é estado da conta. (não repita) Extras: plan, limit, used, overagePrice, upgradeTo. - 402 domain_limit: Limite de apps do plano atingido ao escanear um hostname NOVO. → Remova um app no painel ou faça upgrade. Rondas em apps já cadastrados seguem funcionando. (não repita) Extras: limit, used, upgradeTo. - 403 insufficient_scope: A chave é válida, mas não tem o escopo da rota. → Crie uma chave com o escopo em `required` e troque a variável de ambiente. Escopo não é editável e não chega com o tempo — retry nunca resolve. (não repita) Extras: required. - 403 feature_locked: A ronda pediu um recurso que o plano da conta não inclui: `authenticated` ou `aggressive` (`authenticatedScan`) ou `lgpd` (`lgpdScan`). → Tire a opção do corpo ou faça upgrade pro plano em `upgradeTo`. Não é bug: é estado da conta — e é avaliado a cada chamada, pelo plano daquele instante. (não repita) Extras: feature, upgradeTo. - 403 target_blocked: Alvo na lista de bloqueio (órgão público, banco, grande plataforma) ou identificador interno de integração. Vale mesmo com a declaração de autorização da chave. → Não há o que fazer: a lista não é negociável. (não repita) - 403 domain_unverified: `ai: true`, `authenticated: true` ou `aggressive: true` num app sem prova de propriedade VIGENTE — nunca verificado, ou atestação de provedor vencida/revogada. → Verifique o app no painel (Apps → Verificar) ou refaça com `ai: false` e sem testes autenticados/agressivos. Trate como estado esperado no CI. (corrija e repita) Extras: hostname. - 404 not_found: Ronda inexistente, de outra conta, ou ainda sem relatório. → Confira o id. Em `GET /scans/{id}/report` e `/report.pdf`, espere a ronda terminar (`status: done` em `GET /scans/{id}`). (corrija e repita) - 409 domain_provider_conflict: O hostname é de um projeto gerenciado por uma integração (Vercel ou Lovable). → Dispare a ronda pela tela da integração, no painel. (não repita) Extras: provider. - 429 rate_limited: Muitas rondas em pouco tempo. O balde é por conta: 10 rondas de rajada, reabastecendo 1 por minuto. → Espere o tempo do header `Retry-After` (em segundos) e repita. Insistir antes só renova a espera. (espere o Retry-After) Extras: retryAfterSec. - 500 internal: Erro nosso. → Repita com backoff exponencial. Se persistir, mande o `x-request-id` da resposta pro suporte. (repita com backoff) ## Limites - POST /scans: balde por conta de 10 rondas, reabastecendo 1 por minuto → 429 rate_limited + Retry-After. - Análises com IA (ai: true): cota mensal do plano (Pro 20 · Business 60) → 402 quota_exceeded. Ronda bloqueada/parcial/falha/cancelada NÃO consome. - Recursos de plano em POST /scans: authenticated e aggressive (authenticatedScan), lgpd (lgpdScan) → 403 feature_locked (extras feature, upgradeTo). Hoje Pro e Business incluem os dois. - Apps: Pro 10 · Business sem limite → 402 domain_limit em hostname novo. - Histórico: Pro 365 dias · Business sem limite. - Chaves: 10 ativas por conta. Destinos de webhook: 5 por conta. - Leituras (GET) não têm limite de taxa hoje. ## Webhooks POST na sua URL (https obrigatório, host público) com `content-type: application/json`, `user-agent: Guarita-Webhook/1`, `x-guarita-signature: t=,v1=` e `x-guarita-delivery: dlv_...`. Assinatura: HMAC SHA-256 de `.` com o segredo `gwh_...` do destino (mostrado uma vez). Rejeite se |agora − t| > 300s. Compare em tempo constante. Entrega: 1ª tentativa imediata, timeout 10s, até 6 tentativas com espera 15 s → 30 s → 1 min → 2 min → 4 min. 2xx = ok. 3xx e 4xx (exceto 429) = definitivo, sem retentativa. 429/5xx/rede = retentativa. Redirect não é seguido. Deduplique pelo `id` do evento (evt_…), não pelo id da entrega. Responda 2xx antes de processar. Envelope: { id, event, createdAt, data } ### 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: - scan_id (string): Id da ronda. - hostname (string): Alvo normalizado. - target (string): URL escaneada. - risk_score (integer): Exposição (maior = pior). - risk_label (string): Faixa do score. - critical_count (integer): Achados `critical`. - blocked (boolean): A ronda não viu o app. Trate como inconclusivo, não como "seguro". - report_url (string): Link do relatório no painel. ```json { "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: - scan_id (string): Id da ronda. - hostname (string): Alvo normalizado. - target (string): URL que seria escaneada. - reason (string): A causa, em português. ```json { "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: - scan_id (string): Id da ronda de plantão. - hostname (string): O app. - verdict (string): `breach` = achado novo grave, ou salto de 15+ pontos · `warning` = piorou, sem gravidade · `clean` = primeira ronda (base). - risk_score (integer): Exposição agora. - previous_risk_score (integer | null): Exposição da ronda anterior. `null` quando não havia. - opened (string[]): TÍTULOS dos achados novos (não ids — o título é a chave de identidade entre rondas). - resolved (string[]): Títulos dos achados que sumiram desde a anterior. - blocked (boolean): Sempre `false` (ronda bloqueada não emite). - incomplete (boolean): Sempre `false` (ronda parcial não emite). ```json { "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 } } ``` ## Objetos ### Me Identidade da chave: a conta, a chave, o que ela pode e o plano em vigor. - account_id (string): Id da conta dona da chave. - key_id (string): Id da chave — o mesmo que aparece em Configurações → Desenvolvedores. Use no log da integração pra saber qual chave está rodando sem imprimir o segredo. - scopes (string[]): Escopos da chave, fixados na criação. - plan (string): Plano EFETIVO da conta agora — o mesmo que decide cota e recursos. Na prática vem `pro` ou `business`: sem API no plano, a chave nem autentica. ### App Um app (hostname) monitorado pela conta. - hostname (string): Hostname normalizado: minúsculo, sem `www` e sem porta. - created_at (string): Quando o app entrou na conta — string ISO 8601 em UTC (`2026-08-27T14:09:24.106Z`). - last_scan_at (string | null): Última ronda contra este app. `null` = nunca escaneado. - verified (boolean): A propriedade do domínio já foi comprovada alguma vez (DNS TXT, arquivo, meta tag ou atestação de provedor). Não reconfere a validade de uma atestação — veja `domain_unverified`. ### AppList Lista de apps. Não é paginada: o número de apps é limitado pelo plano. - data (App[]): Apps da conta, na ordem de cadastro. ### ScanListItem Uma ronda no histórico — o resumo que uma automação usa pra decidir. - id (string): Id da ronda (`scan_…`). - hostname (string): Alvo normalizado. - status (string): Estado da ronda. Só leia os números quando for `done`. - created_at (string | null): Quando a ronda foi criada. É o valor do cursor de paginação. - finished_at (string | null): Quando terminou. `null` enquanto roda. - risk_score (integer): Exposição de 0 a 100 — quanto MAIOR, pior. Vem `0` enquanto a ronda não terminou. - risk_label (string): Faixa do número. Vem `SECURE` enquanto a ronda não terminou (é o resumo mínimo, não um veredito). - critical_count (integer): Quantos achados de severidade `critical`. - blocked (boolean): A ronda não conseguiu ver o app (rede bloqueada ou desafio de WAF/anti-bot). O número não representa o app: trate como inconclusivo. ### ScanList Página do histórico de rondas, da mais recente pra mais antiga. - data (ScanListItem[]): As rondas desta página. - next_before (string | null): Cursor da próxima página (o `created_at` do último item). `null` = acabou. ### ScanSummary Resumo derivado do relatório. Formato do scanner (`camelCase`), entregue como ele é. - id (string): Id da ronda. - target (string): URL exata que foi escaneada. - hostname (string): Forma normalizada do alvo. - status (string): Estado da ronda. - score (integer): Exposição de 0 a 100 (maior = pior). - riskLabel (string): Faixa do score. - criticalCount (integer): Achados `critical`. - finishedAt (string, opcional): Quando terminou. - durationMs (integer): Duração da ronda, em milissegundos. - blocked (boolean, opcional): A ronda não viu o app. Ausente = `false`. - blockedReason (string, opcional): Só com `blocked`. `connection` = não alcançamos o app a partir dos nossos servidores (problema do nosso lado; nada a liberar). `challenge` = o app respondeu com desafio ou 403 de WAF/anti-bot (aí vale liberar a Guarita no seu provedor). ### Scan Estado de uma ronda. É o endpoint de polling depois de `POST /scans`. - id (string): Id da ronda. - hostname (string): Alvo normalizado — é por ele que a cota de apps é contada. - target (string): URL exata que você mandou escanear. - status (string): Estado atual. - created_at (string): Quando a ronda foi criada. - finished_at (string | null): Quando terminou. `null` enquanto roda. - progress (ScanProgress | null): Onde a ronda está enquanto roda — a mesma leitura da tela de espera. `null` quando não está rodando (ainda na fila, terminada, falha ou cancelada). - summary (ScanSummary | null): Resumo do resultado. `null` enquanto a ronda não produziu resultado (`queued`, `running`, `analyzing`) e numa ronda `failed` que morreu antes de gerar algo. ### ScanProgress Progresso de uma ronda em execução — o mesmo que a tela de espera mostra. Só existe enquanto a ronda roda. - phase (string): `running` = rodando as verificações · `analyzing` = na análise com IA. - done (integer): Verificações concluídas (fase `running`). - total (integer): Verificações previstas nesta ronda. - module (string | null): Verificação em andamento (`headers`, `cors`, `supabase`…). `null` quando nenhuma está em andamento. - with_ai (boolean): A análise com IA vem depois das verificações (define quantas etapas faltam). - updated_at (string): Última atualização do progresso — string ISO 8601 em UTC (`2026-08-27T14:09:24.106Z`). ### ScanCreateRequest Corpo de `POST /scans`. Só `target` é obrigatório; o resto são as opções da tela, com os mesmos padrões. Booleanos aceitam `true`/`false` e as strings `"true"`/`"false"`. - target (string): URL completa do alvo, começando com `http://` ou `https://`. - ai (boolean, opcional): Análise com IA: explica cada achado e entrega o conserto pronto. Consome 1 análise da cota do mês e exige app verificado. - infra (boolean, opcional): Descoberta e sondagem da infraestrutura por trás do app (Supabase, Firebase, clouds). Só leitura. - authenticated (boolean, opcional): Testes autenticados: a Guarita cria um usuário de teste no seu app e checa, por dentro, se dá pra ver dados de outras pessoas. Exige plano com o recurso (Pro+) e app verificado. Pode disparar o e-mail de boas-vindas do seu app. - aggressive (boolean, opcional): Modo agressivo: confirma as brechas explorando de verdade — cria, altera e apaga dados de teste (e limpa depois). Liga sozinho `authenticated` e `infra`. Exige plano com o recurso (Pro+), app verificado e `authorized_intrusive: true`. Prefira um ambiente de teste. - authorized_intrusive (boolean, opcional): Só é lido com `aggressive: true`: a sua confirmação, POR RONDA, de que tem autorização expressa pra testes intrusivos neste alvo — os Termos (§2) exigem. Sem ela, `400 intrusive_unauthorized`. - lgpd (boolean, opcional): Pré-diagnóstico LGPD: consentimento de cookies, rastreadores, política de privacidade, dado pessoal exposto e dados saindo do país. Exige plano com o recurso (Pro+). São sinais técnicos — não é parecer jurídico. - infra_scope (string[], opcional): Outros endereços SEUS que o app usa (`api.`, `cdn.`…), pra entrarem na sondagem de infra. Array de hostnames ou uma string separada por vírgula. Só o que é seu ou que você tem permissão de testar. ### ScanCreated Resposta `202` de `POST /scans`: a ronda foi aceita e enfileirada. - id (string): Id da ronda nova. Use em `GET /scans/{id}`. - status (string): Estado inicial (`running`). ### ScanCanceled Resposta de `POST /scans/{id}/cancel`: o estado da ronda depois do pedido. - id (string): Id da ronda. - status (string): `canceled` quando a ronda foi interrompida agora. Se já tinha terminado, vem o estado final que ela tinha (`done`, `failed` ou `canceled`). - finished_at (string | null): Quando a ronda parou. ### Report O relatório completo de uma ronda — o mesmo documento que o painel mostra e o PDF imprime. Formato do scanner, versionado por `schemaVersion`: `camelCase` na estrutura e `snake_case` dentro de `ai`. - schemaVersion (string): Versão do formato do relatório. - scan (ReportScan): Metadados da execução. - findings (Finding[]): Os achados, do mais grave pro menos grave. - ai (AiAssessment): Análise da IA. Numa ronda básica (`ai: false`), bloqueada ou parcial, vem com o `risk_score` calculado e os demais blocos vazios. - infra (InfraInventory): Inventário de infraestrutura observado (provedores, ativos, credenciais). - strengths (string[], opcional): O que o app já acerta — boas práticas comprovadas na ronda. - lgpd (LgpdReadiness, opcional): Pré-diagnóstico LGPD. Só quando a ronda incluiu os módulos LGPD (`lgpd: true` em `POST /scans`, ou a opção da tela). ### ReportScan Metadados da execução da ronda. - id (string): Id da ronda. - target (string): URL escaneada. - hostname (string): Alvo normalizado. - stack (string[], opcional): Stack detectada (ex.: `["Lovable", "Supabase", "Vercel"]`). - status (string): Estado. - startedAt (string, opcional): Início da execução. - finishedAt (string, opcional): Fim da execução. - durationMs (integer): Duração em milissegundos. - modulesRun (integer): Quantas verificações rodaram (descontando as puladas). - passive (boolean, opcional): Ronda PASSIVA: só os checks observacionais, porque o domínio não estava verificado (ou excedia a cota de apps verificados do plano). - blocked (boolean, opcional): A ronda não conseguiu ver o app. A IA é pulada e a cota NÃO é consumida. - blockedReason (string, opcional): Motivo do bloqueio (só com `blocked`): `connection` ou `challenge`. - incomplete (boolean, opcional): O tempo esgotou e a ronda terminou PARCIAL (alguns módulos não rodaram). A IA é pulada e a cota não é consumida. ### Finding Um achado da ronda, com a camada leiga e o conserto (quando o plano destrava). - id (string): Id do achado DENTRO desta ronda. Renumera entre rondas — pra casar achados entre rondas use `module` + `title`. - module (string): Verificação que produziu o achado (`secrets`, `cors`, `headers`, `supabase`…). - title (string): Título técnico. Estável entre rondas (é a chave de identidade do achado). - severity (string): Gravidade. - cvss (number): Pontuação CVSS. - description (string): O que foi encontrado, em detalhe. - evidence (string): A prova observada: requisição, resposta, trecho do bundle. Valores sensíveis vêm encurtados com `…` — a ronda não guarda o segredo inteiro. - recommendation (string): Recomendação curta do scanner (não é o conserto pronto). - confidence (string, opcional): `confirmed` = provado por observação ativa · `firm` = detecção determinística · `tentative` = heurística que pede confirmação. Ausente = `firm`. - good (boolean, opcional): Achado POSITIVO: uma boa prática comprovada, não um problema. - links (string[], opcional): Ids de achados relacionados — hoje, os elos de uma cadeia de ataque (`module: "chain"`). - friendly (FindingFriendly, opcional): A versão em português de gente. - fix (FindingFix, opcional): O conserto pronto. Ausente quando retido pelo plano — aí vem `fixWithheld: true`. - fixWithheld (boolean, opcional): O conserto existe, mas não está nesta resposta (plano sem análise com IA). Diz "foi retido", pra não parecer que a ronda não analisou. - fixSample (boolean, opcional): Este é o achado-AMOSTRA da conta: o conserto veio inteiro mesmo sem plano. Uma por conta, não por ronda. - fixFromCatalog (boolean, opcional): O conserto veio do catálogo determinístico, não da IA (ronda básica). ### FindingFriendly O achado em linguagem de produto. - headline (string): Título pra pessoa (sem jargão). - whatItIs (string): O que é. - whyItMatters (string): Por que importa. - urgency (string): Urgência: agora · em breve · quando der. ### FindingFix O conserto pronto pra colar numa IA de código (ou o passo manual, conforme `kind`). - prompt (string): Texto autossuficiente: problema + onde + o que fazer + como validar. É o que se copia e cola no chat da ferramenta. - kind (string, opcional): Tipo de ação: `code` (cola numa IA de código) · `dns` (registro no painel de DNS) · `infra` (console do provedor) · `action` (ação manual) · `confirm` (como confirmar um achado `tentative` antes de mexer) · `chain` (feche qualquer elo da cadeia). - code (string, opcional): Exemplo DIDÁTICO de como a correção costuma ficar. Não é o conserto do seu projeto — quem adapta é a IA, a partir do `prompt`. - targets (string[], opcional): Ferramentas pras quais o prompt foi escrito (`lovable`, `cursor`, `v0`, `bolt`…). ### AiAssessment Análise da IA sobre a ronda. Os blocos abaixo têm formato livre (são a saída do modelo) — leia `risk_score` e `remediation_plan`, que são estáveis. - executive_summary (string): Resumo executivo em português. - risk_score (RiskScore): Exposição consolidada (0–100 + faixa). - remediation_plan (RemediationItem[]): Plano de correção priorizado. - infra_inventory (InfraInventoryEntry[]): Inventário de infra na leitura da IA. - finding_verifications (FindingVerification[], opcional): Achados que a IA verificou ativamente, com veredito. - risk_scenarios (object[]): Cenários de ataque (formato livre). - owasp_mapping (object[]): Mapeamento OWASP (formato livre). - additional_insights (object[]): Observações extras (formato livre). - cross_infra_chains (object[]): Cadeias entre provedores (formato livre). - discovered_vulnerabilities (object[]): Vulnerabilidades descobertas pela IA (formato livre). ### RiskScore Exposição consolidada da ronda. - score (integer): 0 = tranquilo · 100 = muito exposto. Faixas: seguro (0–14) · baixo (15–39) · médio (40–69) · alto (70–100). - label (string): Faixa do score. - justification (string): Por que esse número. ### RemediationItem Um passo do plano de correção. - priority (integer): Ordem (1 = primeiro). - finding_ids (string[]): Achados que este passo fecha. - action (string): O que fazer. - effort (string): Esforço estimado. - impact_if_not_fixed (string): O que acontece se não fizer. - scoreImpact (integer, opcional): Quantos pontos de exposição este passo derruba. ### InfraInventoryEntry Um ativo de infra na leitura da IA. - provider (string): Provedor (`supabase`, `aws`, `vercel`…). - asset (string): O ativo (host, bucket, função). - kind (string): Tipo do ativo. - exposure (string): Como ele está exposto. - notes (string): Observações. ### FindingVerification Veredito da IA sobre um achado que ela testou ativamente. - ref_id (string): Id do achado verificado. - verdict (string): `confirmed` = exploração observada · `refuted` = testado e não se sustenta (alarme falso). - evidence (string): A prova observada. ### InfraInventory Inventário de infraestrutura observado pelo scanner. - assets (InfraAsset[]): Ativos descobertos. - credentials (InfraCredential[]): Credenciais encontradas (valores sensíveis encurtados). - providers (string[]): Provedores detectados. - scopeHosts (string[]): Hosts que entraram no escopo da ronda. - notes (string[]): Observações. ### InfraAsset Um ativo de infraestrutura (projeto BaaS, bucket, função, banco…). - id (string): Id do ativo dentro da ronda. - provider (string): Provedor. - kind (string): Tipo. - endpoint (string): Endereço do ativo. - identifier (string, opcional): Identificador no provedor (id do projeto, nome do bucket). - credentials (InfraCredential[]): Credenciais ligadas a este ativo. - evidence (string): Onde foi observado. - relatedTo (string[]): Ids de ativos relacionados. ### InfraCredential Uma credencial observada. - kind (string): Tipo da credencial. - provider (string): Provedor. - value (string): Valor observado — encurtado quando sensível. - source (string): Onde foi encontrada. - sensitive (boolean): É segredo (não deveria estar público). ### LgpdReadiness Pré-diagnóstico técnico de indicadores LGPD. NÃO é parecer jurídico nem atesta conformidade. - score (integer): Prontidão indicativa (maior = menos indícios de não-conformidade). - label (string): Faixa da prontidão. - categories (object[]): Pontuação por categoria (transparência, consentimento, segurança…). - findingCount (integer): Quantos achados têm relação com a LGPD. - disclaimer (string): O aviso obrigatório: pré-diagnóstico automatizado, não substitui revisão por encarregado/advogado. ### Error Todo erro tem a mesma forma. Trate pelo `error`; mostre a `message`. - error (string): Código estável, feito pro seu código ler. - message (string): Texto em português, feito pra uma pessoa ler no log. ### WebhookEnvelope O corpo de todo `POST` de webhook. - id (string): Id do EVENTO (`evt_` + UUID). Estável entre reentregas — é por ele que se deduplica. - event (string): Qual evento. - createdAt (string): Quando o evento aconteceu — string ISO 8601 em UTC (`2026-08-27T14:09:24.106Z`). - data (object): Carga específica do evento (veja cada um). ## Páginas - [Visão geral](https://www.guarita.dev/docs/api): O que a API cobre, a URL base, os planos e por onde começar. - [Comece em 5 minutos](https://www.guarita.dev/docs/api/quickstart): Crie a chave, confirme com GET /me, dispare uma ronda e leia o resultado. - [Autenticação e escopos](https://www.guarita.dev/docs/api/authentication): A chave gsk_ no header Authorization, o que cada escopo libera, 401 vs 403 e rotação. - [Erros](https://www.guarita.dev/docs/api/errors): O formato de todo erro e o catálogo completo de códigos, com o que fazer em cada um. - [Paginação](https://www.guarita.dev/docs/api/pagination): Cursor por data em GET /scans: limit, before e next_before. - [Limites e cotas](https://www.guarita.dev/docs/api/rate-limits): Rate limit de rondas, cota de análises com IA, limite de apps, chaves e destinos. - [Versionamento](https://www.guarita.dev/docs/api/versioning): O que muda sem aviso, o que nunca muda dentro da v1 e como escrever um cliente tolerante. - [Todos os endpoints](https://www.guarita.dev/docs/api/reference): Os oito endpoints, com método, caminho e escopo — e as convenções que valem pra todos. - [Conta](https://www.guarita.dev/docs/api/reference/me): GET /me — o ping autenticado. - [Apps](https://www.guarita.dev/docs/api/reference/apps): GET /apps — os apps monitorados e o que verified significa. - [Rondas](https://www.guarita.dev/docs/api/reference/scans): GET /scans, POST /scans (com todas as opções da ronda), GET /scans/{id} e POST /scans/{id}/cancel. - [Relatório](https://www.guarita.dev/docs/api/reference/report): GET /scans/{id}/report e /report.pdf — o relatório completo, o PDF e como o plano afeta o conserto. - [Objetos](https://www.guarita.dev/docs/api/reference/objects): Todos os objetos do contrato: Scan, Finding, Report, Error, o envelope do webhook… - [Visão geral](https://www.guarita.dev/docs/api/webhooks): Cadastro do destino, o envelope, os cabeçalhos, entrega e retentativas. - [Eventos](https://www.guarita.dev/docs/api/webhooks/events): scan.completed, scan.failed e finding.opened — quando saem e o que trazem. - [Verificar a assinatura](https://www.guarita.dev/docs/api/webhooks/signature): HMAC SHA-256 sobre t.corpo, janela de 5 minutos e código pronto em Node e Python. - [GitHub Actions](https://www.guarita.dev/docs/api/recipes/github-actions): Rode uma ronda a cada deploy e falhe o pipeline quando aparecer um crítico. - [Changelog](https://www.guarita.dev/docs/api/changelog): Toda mudança no contrato, com data.