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
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
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
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)
| Sinal | Peso |
|---|---|
| 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.
Recebe {"challenge_id": "..."}", devolve uma imagem (base64) com um código de 5 caracteres.
Devolve o mesmo código pra leitura via speechSynthesis do navegador — alternativa de acessibilidade.
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.
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
| Tag | Significado |
|---|---|
| cdn / proxy / edge | Origem por trás de uma CDN/proxy conhecido (ex.: Cloudflare) — esconde o IP real |
| cloud / datacenter / hosting | IP pertence a provedor de nuvem/hospedagem, não a uma conexão residencial |
| vpn / tor | Origem por VPN ou rede Tor |
| email-privacy-relay | E-mail passa por serviço de relay de terceiro (iCloud Hide My Email, DuckDuckGo Email Protection, etc.) |
Limites
| Rota | Limite |
|---|---|
| /api/v1/challenge | 30/min por IP · 120/min por site |
| /api/v1/solve | 60/min por IP |
| /api/v1/visual | 20/min por IP |
| /api/v1/audio | 10/min por IP |
| /api/v1/solve-visual | 20/min por IP |
| /api/v1/verify | 300/min por site |
| /api/v1/analyze | 10/min por IP · 60/min por API key |
Erros comuns
| Código | Situação |
|---|---|
| 404 invalid_site_key | site_key não existe |
| 403 domain_not_allowed | site tem domínios cadastrados e a origem da chamada não bate com nenhum deles |
| 400 invalid_or_expired_challenge | challenge_id inválido, adulterado ou expirado (TTL de 120s) |
| 409 challenge_already_used | esse challenge_id já foi resolvido antes — cada um só gera um response |
| 403 visual_not_unlocked | chamou /visual, /audio ou /solve-visual sem o challenge_id ter passado por /solve e reprovado |
| 401 invalid_secret_key | secret_key errada ou não pertence ao site do challenge |
| 400 invalid_response | token de resposta inválido ou expirado |
| 409 response_already_used | essa resposta já foi verificada antes |
| 429 rate_limited | limite da rota excedido — tente de novo em instantes |
| 401 invalid_api_key | Authorization ausente/inválida em /api/v1/analyze |
| 400 private_or_reserved_target | alvo é um IP privado/loopback/reservado — não pode ser analisado |