Pangeia Sentinela

Documentação da API

Tudo que você precisa pra integrar o Pangeia Sentinela: um script no front, uma chamada no back.

Prefere spec formal? /apidoc/openapi.yaml (OpenAPI 3.0.3, validado) — mesmo padrão do Pangeia ID e da Pangeia PAY.

Visão geral

O fluxo tem três peças: o widget (roda no navegador de quem visita seu site), o challenge/verify (nossa API pública) e o webhook de saída (opcional, avisa seu backend quando uma verificação acontece).

Credenciais por site, geradas no painel: site_key (pública, vai no HTML) e secret_key (privada, só no seu backend — nunca no front).

Widget

GET/widget.js

Cole no seu HTML. O widget cuida do desafio invisível (prova de trabalho + sinais de comportamento) e só escala pro desafio visual/áudio se o score pedir.

<script defer src="https://sentinela.pangeialabs.com/widget.js"></script>
<div class="pangeia-sentinela" data-sitekey="pk_live_..."></div>

O widget cria um campo oculto pangeia-sentinela-response dentro da div — é esse valor que seu formulário envia junto com o resto dos dados, e que seu backend repassa pro /api/v1/verify.

Integrações feitas antes do rebrand (classe pangeia-captcha, campo pangeia-captcha-response) continuam funcionando sem mudança nenhuma — o widget emite os dois nomes.

Challenge

POST/api/v1/challenge

Chamado automaticamente pelo widget — você normalmente não precisa chamar isso na mão.

curl -X POST https://sentinela.pangeialabs.com/api/v1/challenge \
  -H "Content-Type: application/json" \
  -d '{"site_key": "pk_live_..."}'

# resposta
{"ok": true, "challenge_id": "<jwt>", "seed": "...", "difficulty": 4}

Verify

POST/api/v1/verify

Chamado pelo seu backend, servidor-a-servidor, depois que o formulário chega com o pangeia-sentinela-response preenchido.

curl -X POST https://sentinela.pangeialabs.com/api/v1/verify \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"challenge_id": "...", "response": "..."}'

# resposta (sucesso)
{"success": true, "score": 12}

# resposta (erro)
{"success": false, "error": "response_already_used"}

Cada response só pode ser verificado uma vez — tentativas repetidas com o mesmo valor recebem 409.

Critério de score (documentado, sem caixa-preta)

SinalPeso
Prova de trabalho ausente ou incorreta+40
Resposta em menos de 400ms (rápido demais pra ser humano)+30
Honeypot preenchido+50
Nenhum movimento de ponteiro registrado+20

Score ≥ 50 reprova o desafio invisível e escala pro fallback visual.

Fallback visual/áudio

Disparado automaticamente pelo widget quando o score do desafio invisível pede — geralmente você não chama isso direto.

Essas três rotas só respondem pra um challenge_id que já passou por /api/v1/solve e recebeu needs_visual: true — chamar direto, sem isso, devolve 403 visual_not_unlocked.

POST/api/v1/visual

Recebe {"challenge_id": "..."}", devolve uma imagem (base64) com um código de 5 caracteres.

POST/api/v1/audio

Devolve o mesmo código pra leitura via speechSynthesis do navegador — alternativa de acessibilidade.

POST/api/v1/solve-visual

Recebe {"challenge_id", "answer"}, devolve um response pra usar no /api/v1/verify se acertar — uso único: um challenge_id só gera um response, seja por aqui ou por /api/v1/solve.

Webhook de saída

Configure a URL no painel, na página do seu site. Toda verificação concluída dispara um POST assinado:

POST /seu-endpoint HTTP/1.1
Content-Type: application/json
X-Pangeia-Signature: <hmac-sha256 hex>

{
  "event_id": "...",
  "type": "verification.completed",
  "site_key": "pk_live_...",
  "success": true,
  "score": 12,
  "timestamp": 1784650000
}

Validando a assinatura (Python)

import hmac, hashlib

def valido(corpo_bruto: bytes, assinatura: str, secret: str) -> bool:
    esperado = hmac.new(secret.encode(), corpo_bruto, hashlib.sha256).hexdigest()
    return hmac.compare_digest(assinatura, esperado)

O webhook_secret fica visível na página do site no painel. Guarde event_id pra idempotência — reentregas usam o mesmo id.

Sentinela: análise de infraestrutura

Recebe um e-mail, hostname ou IP e devolve a cadeia de infraestrutura por trás dele (MX → CNAME → A/AAAA → PTR → ASN → RDAP → provedor) mais um veredito: um humano real não acessa a partir de nuvem/datacenter/proxy/VPN/Tor nem usa um e-mail que passa por relay de privacidade de terceiro — se qualquer um desses sinais aparecer em qualquer ponto da cadeia, o veredito é block.

POST/api/v1/analyze

Autenticado por API key de developer (gerada no painel), servidor-a-servidor.

curl -X POST https://sentinela.pangeialabs.com/api/v1/analyze \
  -H "Authorization: Bearer dnsk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"target": "usuario@exemplo.com", "type": "auto"}'

# resposta
{
  "ok": true,
  "result": { "target": "usuario@exemplo.com", "type": "email", "...": "..." },
  "verdict": "block",
  "matched_tags": ["cdn", "proxy"]
}

type é opcional (auto por padrão) — pode ser forçado como email, hostname ou ip. Resultado sempre inclui a cadeia completa de evidência (nunca só um booleano) e um campo cached quando a resposta veio do cache.

Tags que geram bloqueio

TagSignificado
cdn / proxy / edgeOrigem por trás de uma CDN/proxy conhecido (ex.: Cloudflare) — esconde o IP real
cloud / datacenter / hostingIP pertence a provedor de nuvem/hospedagem, não a uma conexão residencial
vpn / torOrigem por VPN ou rede Tor
email-privacy-relayE-mail passa por serviço de relay de terceiro (iCloud Hide My Email, DuckDuckGo Email Protection, etc.)

Limites

RotaLimite
/api/v1/challenge30/min por IP · 120/min por site
/api/v1/solve60/min por IP
/api/v1/visual20/min por IP
/api/v1/audio10/min por IP
/api/v1/solve-visual20/min por IP
/api/v1/verify300/min por site
/api/v1/analyze10/min por IP · 60/min por API key

Erros comuns

CódigoSituação
404 invalid_site_keysite_key não existe
403 domain_not_allowedsite tem domínios cadastrados e a origem da chamada não bate com nenhum deles
400 invalid_or_expired_challengechallenge_id inválido, adulterado ou expirado (TTL de 120s)
409 challenge_already_usedesse challenge_id já foi resolvido antes — cada um só gera um response
403 visual_not_unlockedchamou /visual, /audio ou /solve-visual sem o challenge_id ter passado por /solve e reprovado
401 invalid_secret_keysecret_key errada ou não pertence ao site do challenge
400 invalid_responsetoken de resposta inválido ou expirado
409 response_already_usedessa resposta já foi verificada antes
429 rate_limitedlimite da rota excedido — tente de novo em instantes
401 invalid_api_keyAuthorization ausente/inválida em /api/v1/analyze
400 private_or_reserved_targetalvo é um IP privado/loopback/reservado — não pode ser analisado