▸ API · Fundamentos

Erros

O formato de todo erro e o catálogo completo de códigos, com o que fazer em cada um.

atualizado em 2 set 2026versão v1

O formato

Todo erro responde JSON com a mesma forma: error, um código estável feito pro seu código ler, e message, um texto em português feito pra uma pessoa ler no log. Alguns trazem campos extras (listados no catálogo). Trate pelo error; nunca pelo texto da mensagem, que pode mudar.

401 · application/json
{
  "error": "unauthorized",
  "message": "Chave de API ausente ou inválida. Envie `Authorization: Bearer gsk_...` — crie a sua em Configurações > Desenvolvedores."
}
CampoDescrição
error
stringsempre
Código estável, feito pro seu código ler.
message
stringsempre
Texto em português, feito pra uma pessoa ler no log.

O status HTTP acompanha o código (400 entrada, 401 credencial, 402 estado da conta, 403 permissão, 404 não existe, 409 conflito, 429 limite, 500 nosso). Toda resposta — sucesso ou erro — traz o header x-request-id.

Repetir ou não

Cada código diz o que fazer com a chamada. A regra curta:

ClasseO que significaO que fazer
400 · 404A requisição está errada, ou o recurso não existe (ainda).Corrija e repita. Em 404 de relatório, espere a ronda terminar.
401 · 403Credencial, permissão ou recurso do plano.Repetir sem mudar nada não resolve. 401 = confira variável, chave e plano · 403 = escopo que falta (insufficient_scope), recurso fora do plano (feature_locked), app sem verificação (domain_unverified) ou alvo bloqueado (target_blocked).
402 · 409Estado da conta (cota, limite de apps, app gerenciado por integração).Não é bug. Avise quem cuida da assinatura; em cota, refaça com ai: false.
429Muitas rondas em pouco tempo.Espere os segundos do header Retry-After e repita.
500Erro nosso.Repita com backoff exponencial (1 s, 2 s, 4 s…). Se persistir, mande o x-request-id pro suporte.

Catálogo

Todos os códigos que a API pública devolve. A lista é fechada e testada contra o código: um código novo entra aqui antes de entrar em produção.

HTTPerrorOndeAção
400bad_requestqualquer rotacorrija e repita
400invalid_targetPOST /scanscorrija e repita
400intrusive_unauthorizedPOST /scanscorrija e repita
400invalid_cursorGET /scanscorrija e repita
401unauthorizedqualquer rotanão repita
402quota_exceededPOST /scansnão repita
402domain_limitPOST /scansnão repita
403insufficient_scopequalquer rotanão repita
403feature_lockedPOST /scansnão repita
403target_blockedPOST /scansnão repita
403domain_unverifiedPOST /scanscorrija e repita
404not_foundGET /scans/{id}, POST /scans/{id}/cancel, GET /scans/{id}/report, GET /scans/{id}/report.pdfcorrija e repita
409domain_provider_conflictPOST /scansnão repita
429rate_limitedPOST /scansespere o Retry-After
500internalqualquer rotarepita com backoff

400bad_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.

O que fazer: Corrija a requisição. (corrija e repita)

Onde: qualquer rota

400 · application/json
{
  "error": "bad_request",
  "message": "Unexpected token i in JSON at position 2"
}

400invalid_target

target ausente ou não é URL completa (http:// ou https://).

O que fazer: Mande a URL inteira do alvo. (corrija e repita)

Onde: POST /scans

400 · application/json
{
  "error": "invalid_target",
  "message": "Informe `target` como URL completa, começando com https://"
}

400intrusive_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.

O que fazer: Mande authorized_intrusive: true junto — só se você tem autorização expressa pra testes intrusivos neste alvo. Ou tire o aggressive. (corrija e repita)

Onde: POST /scans

400 · application/json
{
  "error": "intrusive_unauthorized",
  "message": "O modo agressivo executa testes intrusivos que mutam estado no alvo. Confirme que você tem autorização explícita para testá-lo dessa forma."
}

400invalid_cursor

before não é uma data ISO 8601.

O que fazer: Use o next_before que veio na página anterior. (corrija e repita)

Onde: GET /scans

400 · application/json
{
  "error": "invalid_cursor",
  "message": "`before` precisa ser uma data ISO 8601 (ex.: 2026-08-29T12:00:00.000Z) — use o `next_before` que veio na página anterior."
}

401unauthorized

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.

O que fazer: 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)

Onde: qualquer rota

401 · application/json
{
  "error": "unauthorized",
  "message": "Chave de API ausente ou inválida. Envie `Authorization: Bearer gsk_...` — crie a sua em Configurações > Desenvolvedores."
}

402quota_exceeded

Cota de análises com IA do mês esgotada (só com ai: true).

O que fazer: 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)

Onde: POST /scans

CampoDescrição
plan
stringsempre
Plano atual.
limit
integersempre
Cota do período.
used
integersempre
Quanto já foi usado.
overagePrice
numbersempre
R$ por análise extra (0 = sem excedente no plano).
upgradeTo
string | nullsempre
Plano sugerido.
402 · application/json
{
  "error": "quota_exceeded",
  "message": "Você usou suas 20 análises com IA do mês. Faça upgrade pra liberar mais.",
  "plan": "pro",
  "limit": 20,
  "used": 20,
  "overagePrice": 15,
  "upgradeTo": "business"
}

402domain_limit

Limite de apps do plano atingido ao escanear um hostname NOVO.

O que fazer: Remova um app no painel ou faça upgrade. Rondas em apps já cadastrados seguem funcionando. (não repita)

Onde: POST /scans

CampoDescrição
limit
integersempre
Apps que o plano cobre.
used
integersempre
Apps em uso.
upgradeTo
string | nullsempre
Plano sugerido.
402 · application/json
{
  "error": "domain_limit",
  "message": "Seu plano cobre 10 apps. Remova um app em Apps ou faça upgrade pra escanear outro.",
  "limit": 10,
  "used": 10,
  "upgradeTo": "business"
}

403insufficient_scope

A chave é válida, mas não tem o escopo da rota.

O que fazer: 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)

Onde: qualquer rota

CampoDescrição
required
stringsempre
O escopo que falta.
scans:readscans:writeapps:read
403 · application/json
{
  "error": "insufficient_scope",
  "message": "Esta chave não tem a permissão \"scans:write\".",
  "required": "scans:write"
}

403feature_locked

A ronda pediu um recurso que o plano da conta não inclui: authenticated ou aggressive (authenticatedScan) ou lgpd (lgpdScan).

O que fazer: 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)

Onde: POST /scans

CampoDescrição
feature
stringsempre
O recurso que faltou (authenticatedScan, lgpdScan).
upgradeTo
string | nullsempre
Plano sugerido. null quando não há plano acima.
403 · application/json
{
  "error": "feature_locked",
  "message": "Seu plano não inclui o modo agressivo.",
  "feature": "authenticatedScan",
  "upgradeTo": "pro"
}

403target_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.

O que fazer: Não há o que fazer: a lista não é negociável. (não repita)

Onde: POST /scans

403 · application/json
{
  "error": "target_blocked",
  "message": "Este alvo está na lista de bloqueio da Guarita — escaneie só apps que são seus."
}

403domain_unverified

ai: true, authenticated: true ou aggressive: true num app sem prova de propriedade VIGENTE — nunca verificado, ou atestação de provedor vencida/revogada.

O que fazer: 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)

Onde: POST /scans

CampoDescrição
hostname
stringsempre
O app em questão.
403 · application/json
{
  "error": "domain_unverified",
  "message": "Testes intrusivos ou autenticados exigem comprovar que app.exemplo.com.br é seu. Verifique a propriedade do app em Apps e tente de novo.",
  "hostname": "app.exemplo.com.br"
}

404not_found

Ronda inexistente, de outra conta, ou ainda sem relatório.

O que fazer: Confira o id. Em GET /scans/{id}/report e /report.pdf, espere a ronda terminar (status: done em GET /scans/{id}). (corrija e repita)

Onde: GET /scans/{id}, POST /scans/{id}/cancel, GET /scans/{id}/report, GET /scans/{id}/report.pdf

404 · application/json
{
  "error": "not_found",
  "message": "Ronda não encontrada."
}

409domain_provider_conflict

O hostname é de um projeto gerenciado por uma integração (Vercel ou Lovable).

O que fazer: Dispare a ronda pela tela da integração, no painel. (não repita)

Onde: POST /scans

CampoDescrição
provider
stringsempre
Qual integração gerencia o app.
vercellovable
409 · application/json
{
  "error": "domain_provider_conflict",
  "message": "Este endereço já é gerenciado pela Vercel. Abra o app integrado em Apps em vez de cadastrá-lo novamente por URL.",
  "provider": "vercel"
}

429rate_limited

Muitas rondas em pouco tempo. O balde é por conta: 10 rondas de rajada, reabastecendo 1 por minuto.

O que fazer: Espere o tempo do header Retry-After (em segundos) e repita. Insistir antes só renova a espera. (espere o Retry-After)

Onde: POST /scans

CampoDescrição
retryAfterSec
integersempre
Segundos até a próxima ronda caber. O mesmo valor vai no header Retry-After.
429 · application/json
{
  "error": "rate_limited",
  "message": "Muitas rondas em pouco tempo. Aguarde ~42s e tente de novo — o limite é de uso justo, pra manter a ronda básica grátis pra todo mundo.",
  "retryAfterSec": 42
}

500internal

Erro nosso.

O que fazer: Repita com backoff exponencial. Se persistir, mande o x-request-id da resposta pro suporte. (repita com backoff)

Onde: qualquer rota

500 · application/json
{
  "error": "internal",
  "message": "Erro interno."
}

Falar com o suporte

Escreva pra help@guarita.dev com o x-request-id da resposta, o endpoint e o horário. Com o id a gente acha a linha de log em segundos; sem ele, procura no escuro. Nunca mande a chave — o key_id de GET /me basta.

Achou algo errado ou faltando? help@guarita.dev — com o x-request-id, se for sobre uma resposta.