API de detecção de fraude por IP: pontue cadastros, logins e checkouts em Node e Python
Adicione uma API de detecção de fraude por IP ao cadastro, login e checkout: código em Node e Python que rodamos em seis IPs públicos, com saída real, cache e limites de taxa.

Dois cadastros chegam com um segundo de diferença, e ambos escondem o endereço de casa da pessoa. Um vem de uma saída Tor em Hamburgo. O outro vem do iCloud Private Relay, que a Apple descreve como "part of an iCloud+ subscription" (parte de uma assinatura do iCloud+) que protege você "when you browse the web in Safari" (quando você navega na web no Safari) (Suporte da Apple). Uma regra que bloqueia "qualquer coisa oculta" rejeita os dois, e um deles é um cliente pagante da Apple.
Distinguir esses dois é o trabalho. É uma fatia do que a inteligência de IP cobre, e cabe em um único arquivo com menos de 50 linhas. Abaixo estão a requisição, uma resposta real, uma versão em Node e outra em Python com cache e tratamento de falhas, o que elas imprimiram para seis endereços e onde a verificação deixa a desejar.

A única requisição de que uma API de detecção de fraude por IP precisa
A integração inteira é um único GET, com o IP no caminho e a sua chave no cabeçalho x-api-key. A referência da API documenta o mesmo formato:
curl https://api.geoiphub.com/v1/lookup/45.83.220.7 \
-H "x-api-key: YOUR_API_KEY"
Três detalhes pegam as pessoas na primeira tentativa:
- O IP vai no caminho.
/v1/lookup/{ip}é a única forma. Uma versão com query string redireciona em vez de retornar dados. - Não envie o placeholder. Enviamos a string literal
YOUR_API_KEYe recebemos401com{"message":"Invalid API key"}. Deixe o cabeçalho de fora até ter uma chave real; chamadas anônimas funcionam a 30 por minuto. - Um IP por chamada. Cada requisição recebe um endereço, então um script que verifica uma lista faz uma requisição por endereço, e cada uma conta para o seu limite.
Crie uma chave gratuita no painel quando passar da fase de testes. O plano gratuito dá a essa chave 1.500 consultas por dia, com todos os campos incluídos.
O que voltou para um endereço da Mullvad, e em quais campos basear a decisão
A resposta tem cerca de 2,7 KB de JSON aninhado em dez grupos, e uma verificação de fraude precisa de dois deles: detection e scoring. Esta é a consulta de 45.83.220.7, reduzida, mas com os valores intactos:
{
"ip": "45.83.220.7",
"asn": {
"asn": 39351,
"org": "ESAB-AS - 31173 Services AB",
"asn_type": "hosting",
"connection_type": "datacenter"
},
"geo": {
"country_code": "SE",
"city": "Malmö"
},
"detection": {
"is_vpn": true,
"is_proxy": false,
"is_tor": false,
"is_hosting": true,
"is_relay": false,
"vpn_provider": "Mullvad (31173 Services)",
"vpn_confidence": 85
},
"scoring": {
"fraud_score": 46,
"confidence": 0.56,
"recommended_action": "review",
"detection_methods": [
"known_vpn_server",
"datacenter_ip"
]
},
"meta": {
"last_classified_at": "2026-10-16T07:40:17.524Z"
}
}
Este endereço fica na 31173 Services, a rede em que a Mullvad VPN opera seus servidores, em Malmö. Quando verificamos em 13 de outubro, ele não estava na lista de servidores publicada pela própria Mullvad. Dois sinais dispararam. known_vpn_server veio da nossa tabela de redes de hospedagem de VPN conhecidas, e é por isso que vpn_confidence é 85, e não o 95 que uma correspondência com a lista de servidores da própria Mullvad daria. datacenter_ip diz que é hospedagem, não uma conexão doméstica. Juntos, eles somam 46, o que cai em review: uma VPN, por si só, não é fraude.
Três campos carregam a decisão:
| Campo | O que contém | Como o código o usa |
|---|---|---|
scoring.recommended_action | allow (pontuação até 25), review (até 50), step_up (até 75), block (acima de 75) | A ramificação |
scoring.fraud_score | O composto de 0 a 100 por trás dessa ação | Registro em log e ajuste posterior |
scoring.detection_methods | Os sinais que dispararam, como tor_exit ou openvpn_open | O motivo que você armazena e mostra aos revisores |
Os indicadores de detection (is_vpn, is_proxy, is_tor, is_hosting, is_relay) são os fatos brutos por trás da pontuação. Use-os nas poucas regras em que o seu negócio difere das faixas padrão. A pontuação também tem sinais atenuantes: um ISP residencial subtrai 20 pontos e um rastreador verificado, 30, então um indicador sozinho não empurra um endereço para block. O guia da pontuação de fraude de IP explica o que move o número.
A versão em Node: fetch, um cache de uma hora e quatro ramificações
Este é exatamente o arquivo que rodamos, no Node 24, sem dependências. Ele exige o Node 18 ou superior por causa do fetch nativo:
// screen.mjs: score an IP before a signup, login or checkout. Node 18+, no dependencies.
const API = "https://api.geoiphub.com/v1/lookup/";
const KEY = process.env.GEOIPHUB_API_KEY; // without a key: 30 lookups a minute, for testing only
const TTL_MS = 60 * 60 * 1000; // reuse a verdict for an hour
const cache = new Map(); // ip -> { at, data }
async function lookup(ip) {
const hit = cache.get(ip);
if (hit && Date.now() - hit.at < TTL_MS) return hit.data;
const headers = KEY ? { "x-api-key": KEY } : {};
const res = await fetch(API + encodeURIComponent(ip), {
headers,
signal: AbortSignal.timeout(3000),
});
if (res.status === 429) throw new Error(`rate limited, retry after ${res.headers.get("retry-after")}s`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
cache.set(ip, { at: Date.now(), data });
return data;
}
export async function screen(ip, flow) {
let data;
try {
data = await lookup(ip);
} catch (err) {
return { action: "review", reason: `lookup failed: ${err.message}` }; // fail soft, never block on our outage
}
const { scoring, detection } = data;
let action = scoring.recommended_action; // "allow" | "review" | "step_up" | "block"
if (detection.is_tor) action = "block"; // no step-up through a Tor exit
if (flow === "checkout" && action === "review") action = "step_up"; // stricter where money moves
const flags = ["is_vpn", "is_proxy", "is_tor", "is_hosting", "is_relay"].filter((f) => detection[f]);
return { action, score: scoring.fraud_score, flags, methods: scoring.detection_methods };
}
// Try it: node screen.mjs signup 45.33.32.156 104.28.46.99
const [flow, ...ips] = process.argv.slice(2);
for (const ip of ips) {
const v = await screen(ip, flow);
console.log(ip.padEnd(15), v.action.padEnd(8), v.score ?? "-", v.flags?.join(",") || "-", v.methods?.join(",") || v.reason);
}
Quatro decisões nele são deliberadas:
- Ele parte de
recommended_actione o sobrescreve em apenas dois lugares. Uma saída Tor sempre é bloqueada, e no checkoutreviewvirastep_up, porque é ali que o dinheiro sai. - Ele falha de forma branda. Um timeout, um 5xx ou um 429 retorna
review, nãoblock. Se a nossa API estiver inacessível, os seus cadastros continuam funcionando e ficam registrados para uma segunda análise. - O timeout é de 3 segundos. Do nosso lado, um IP conhecido é respondido a partir de um banco de dados mapeado em memória em menos de um milissegundo, então a ida e volta pela rede é a maior parte da espera. Um IP que nunca vimos é classificado ao vivo em menos de 2,5 segundos, e 3 segundos cobrem esse caso.
- A chave fica no seu servidor. Chame a API a partir do seu backend, nunca de código no navegador, onde qualquer pessoa consegue ler a chave.
A versão em Python precisa de um User-Agent, ou recebe um 403
Nossa API responde ao User-Agent padrão do urllib do Python, Python-urllib/3.x, com 403 Forbidden. Reproduzimos isso em 16 de outubro, às 05:36 UTC, com uma chamada simples a urllib.request.urlopen: HTTP Error 403: Forbidden. Curl e requests não esbarram nisso, porque enviam User-Agents diferentes. Por isso o arquivo Python define o seu próprio, com um comentário explicando o motivo:
# screen.py: score an IP before a signup, login or checkout. Python 3.8+, standard library only.
import json, os, sys, time, urllib.error, urllib.request
API = "https://api.geoiphub.com/v1/lookup/"
KEY = os.environ.get("GEOIPHUB_API_KEY") # without a key: 30 lookups a minute, for testing only
TTL = 3600 # reuse a verdict for an hour
_cache = {} # ip -> (time, data)
def lookup(ip):
hit = _cache.get(ip)
if hit and time.time() - hit[0] < TTL:
return hit[1]
# Set a User-Agent: our API answers urllib's default "Python-urllib/3.x" with 403 Forbidden.
headers = {"User-Agent": "signup-screen/1.0"}
if KEY:
headers["x-api-key"] = KEY
req = urllib.request.Request(API + ip, headers=headers)
with urllib.request.urlopen(req, timeout=3) as resp:
data = json.load(resp)
_cache[ip] = (time.time(), data)
return data
def screen(ip, flow):
try:
data = lookup(ip)
except urllib.error.HTTPError as e:
wait = e.headers.get("Retry-After") if e.code == 429 else None
return {"action": "review", "reason": f"HTTP {e.code}" + (f", retry after {wait}s" if wait else "")}
except (OSError, ValueError) as e: # network error, timeout, bad JSON
return {"action": "review", "reason": f"lookup failed: {e}"} # fail soft
scoring, detection = data["scoring"], data["detection"]
action = scoring["recommended_action"] # "allow" | "review" | "step_up" | "block"
if detection["is_tor"]:
action = "block" # no step-up through a Tor exit
if flow == "checkout" and action == "review":
action = "step_up" # stricter where money moves
flags = [f for f in ("is_vpn", "is_proxy", "is_tor", "is_hosting", "is_relay") if detection[f]]
return {"action": action, "score": scoring["fraud_score"], "flags": flags,
"methods": scoring["detection_methods"]}
if __name__ == "__main__":
flow, ips = sys.argv[1], sys.argv[2:]
for ip in ips:
v = screen(ip, flow)
print(f"{ip:<15} {v['action']:<8}", v.get("score", "-"), ",".join(v.get("flags", [])) or "-",
",".join(v.get("methods", [])) or v.get("reason", ""))
É a mesma lógica do arquivo Node, usando apenas a biblioteca padrão, testada no Python 3.9.6. O urllib lança HTTPError para qualquer status que não seja 2xx, então o 429 e o seu cabeçalho Retry-After são lidos a partir da exceção.
O que os dois scripts imprimiram para seis endereços públicos
Os dois scripts deram o mesmo veredito para todos os endereços. Rodamos o arquivo Node às 07:39 UTC e o arquivo Python às 07:40 UTC em 16 de outubro de 2026, sem chave, contra seis endereços públicos. O último comando roda de novo o endereço da Mullvad como checkout:
node screen.mjs signup 45.33.32.156 104.28.46.99 45.83.220.7 91.90.122.25 102.129.235.17 185.220.101.1
node screen.mjs checkout 45.83.220.7
45.33.32.156 allow 10 is_hosting datacenter_ip
104.28.46.99 allow 9 is_relay relay_ip
45.83.220.7 review 46 is_vpn,is_hosting known_vpn_server,datacenter_ip
91.90.122.25 step_up 64 is_hosting datacenter_blocklisted,no_ptr_datacenter,known_spammer
102.129.235.17 block 92 is_vpn known_vpn_server,openvpn_open,ipsec_open
185.220.101.1 block 100 is_tor,is_hosting tor_exit,datacenter_blocklisted,no_ptr_datacenter,known_spammer
45.83.220.7 step_up 46 is_vpn,is_hosting known_vpn_server,datacenter_ip
python3 screen.py com os mesmos argumentos imprimiu as mesmas sete linhas, caractere por caractere. Veja o que é cada endereço e por que ele caiu onde caiu:
| Endereço | O que é | Ação | Sinais que dispararam |
|---|---|---|---|
45.33.32.156 | scanme.nmap.org, o host de teste público do Nmap na Linode | allow, 10 | Apenas datacenter_ip. Hospedagem, por si só, não é fraude |
104.28.46.99 | Egress do iCloud Private Relay na Cloudflare | allow, 9 | relay_ip. A pontuação limita endereços de relay a 60, então eles nunca chegam a block |
45.83.220.7 | Rede da Mullvad em Malmö | review, 46; step_up no checkout | known_vpn_server, datacenter_ip |
91.90.122.25 | Servidor da PIA VPN em Schaan, Liechtenstein, na M247 | step_up, 64 | datacenter_blocklisted, no_ptr_datacenter, known_spammer. Não é sinalizado como VPN |
102.129.235.17 | Servidor de VPN comercial em Washington, D.C. | block, 92 | known_vpn_server, mais openvpn_open e ipsec_open: nossas sondagens concluíram handshakes OpenVPN e IPsec |
185.220.101.1 | Saída Tor em Hamburgo | block, 100 | tor_exit, datacenter_blocklisted, no_ptr_datacenter, known_spammer |
Os dois cadastros do início ficaram em extremos opostos: a saída Tor com 100 e o usuário do Private Relay com 9. Essa distância é o motivo para pontuar em vez de bloquear todo endereço oculto.
Os resultados mudam conforme os nossos dados mudam. Até esta reescrita, este post mostrava 45.83.220.7 como hospedagem comum, com pontuação 10 e allow. Em 8 de outubro, nosso scanner já o tinha associado à Mullvad, e agora ele recebe 46. Registre a ação e os métodos junto com cada decisão, para que uma mudança assim apareça nos seus dados em vez de pegar você de surpresa.
Onde chamar: em todo cadastro, login e checkout
Pontue todo cadastro, todo login e todo checkout, no servidor, antes de efetivar a ação. Esses são os três momentos em que um fraudador ganha alguma coisa, e eles são raros o suficiente por usuário para caber em uma cota diária. Não chame a API em toda visualização de página.
O que cada ação significa na prática:
| Ação | Cadastro | Login | Checkout |
|---|---|---|---|
allow | Criar a conta | Fazer o login | Processar o pagamento |
review | Criar a conta e marcar para revisão | Fazer o login e registrar o motivo | Nosso código a promove para step_up |
step_up | Verificar e-mail ou telefone primeiro | Pedir o segundo fator | Executar o 3-D Secure |
block | Recusar com uma mensagem neutra | Recusar e contar para o bloqueio da conta | Recusar o pedido |
As faixas padrão são um ponto de partida, e os seus próprios dados de fraude devem ajustá-las. Nossos limites da pontuação de fraude para login, cadastro e checkout trazem cortes iniciais por ação e o raciocínio por trás de cada um.
Pontue também o endereço certo. Atrás de um load balancer ou CDN, o endereço do socket é o do seu proxy, e o IP do cliente fica em um cabeçalho de encaminhamento que qualquer cliente também pode forjar. Pegue-o do salto que o seu próprio proxy confiável adicionou, nunca do valor mais à esquerda enviado por um cliente.
Ficando abaixo de 1.500 consultas por dia: cache e limite de taxa
O plano gratuito permite 1.500 consultas por dia. Faça cache por IP no seu próprio código e a cota rende muito mais, porque uma pessoa normalmente se cadastra, faz login e paga a partir do mesmo endereço em menos de uma hora.
A própria API não ajuda no cache: toda resposta traz Cache-Control: private, no-store, então nenhum cache HTTP compartilhado vai armazená-la. Os dois scripts mantêm um veredito por uma hora na memória do processo. Use Redis ou o cache que você já tem se rodar mais de um servidor. Não mantenha um veredito por mais de um dia, porque nossos feeds de ameaças e o banco de dados de consulta são atualizados diariamente.
Os dois limites se comportam de formas diferentes, e esbarramos em um deles enquanto testávamos este post:
| Chamando como | Limite | O que acontece ao ultrapassá-lo |
|---|---|---|
| Sem chave | 30 consultas por minuto por IP de origem | A 30ª requisição dentro de um minuto retorna 429 e bloqueia esse IP por duas horas |
| Chave gratuita | 1.500 consultas por dia | 429 com um cabeçalho Retry-After até o contador diário zerar |
Às 05:37 UTC de 16 de outubro, disparamos uma rajada de requisições anônimas para ver o 429. Em vinte segundos, a API retornou 429 com Retry-After: 7199, e todas as chamadas anônimas a partir do nosso IP ficaram bloqueadas por duas horas, inclusive as dos nossos próprios scripts. O caminho de falha branda deles foi a primeira saída real que obtivemos:
45.33.32.156 review - - lookup failed: rate limited, retry after 6971s
45.33.32.156 review - - HTTP 429, retry after 6970s
Por isso, trate o limite anônimo como uma cota para testes e use uma chave em produção. Baseie a lógica no código de status e no Retry-After, não no texto da mensagem do 429.
Onde esta verificação deixa a desejar
Um veredito de IP é um único sinal, e ele deixa coisas passarem. Quatro casos surgiram enquanto escrevíamos este post:
- Um servidor de VPN listado pode voltar limpo. O servidor da PIA
91.90.122.25estava na lista publicada pela própria PIA em 4 de outubro, mas a nossa API retornais_vpn: false. Ele só chegou astep_upporque está em uma blocklist DNS. Sem esse histórico, teria passado como hospedagem comum. - Proxies residenciais são, em grande parte, invisíveis para dados de IP. Tráfego roteado por conexões domésticas reais parece um usuário doméstico. O campo
is_residential_proxyé uma inferência grosseira a partir da rede, não uma detecção, então não crie uma regra com base nele. - Endereços compartilhados carregam o histórico de outras pessoas. Operadoras móveis e CGNAT colocam muitos usuários atrás de um único IP, e é por isso que a pontuação tem teto nas faixas de CGNAT e que
blockdeve continuar raro nelas. - As localizações de relay são aproximadas. Para
104.28.46.99, nossa geolocalização diz Luanda, Angola, enquanto a própria lista de egress da Apple coloca essa faixa em Lusaka, Zâmbia. Use o indicador de relay, não a cidade, para decidir, como explica o nosso guia para deixar o Private Relay passar.
Combine a verificação de IP com o que você sabe sobre a conta: idade do e-mail, dispositivo, histórico de pagamentos e velocidade. O guia da pilha de detecção de fraude em pagamentos mostra onde os sinais de IP se encaixam entre eles.
Rode primeiro contra o seu próprio tráfego
Antes de integrar a um fluxo em produção, rode qualquer um dos scripts sobre um dia de IPs de cadastros anteriores com uma chave gratuita e compare as ações com o que você já sabe sobre essas contas. Se block teria pegado contas que você depois baniu, e allow cobre os clientes que você manteve, coloque a verificação na frente do cadastro primeiro, depois do login e, por fim, do checkout. Para verificar um endereço manualmente, o verificador de pontuação de fraude de IP mostra a mesma pontuação e os mesmos motivos, sem nenhum código.
Perguntas frequentes
A API da GeoIPHub é gratuita para começar?
Sim. O plano gratuito inclui 1.500 consultas por dia com todos os campos de dados, uma chave de API e nenhum cartão de crédito. Sem chave, você pode fazer 30 consultas por minuto para testes.
Em qual campo o meu código deve basear a decisão?
scoring.recommended_action. Ele é allow até a pontuação 25, review até 50, step_up até 75 e block acima de 75. Leia scoring.detection_methods para registrar o motivo.
Por que o meu script Python recebe um 403?
Nossa API responde ao User-Agent padrão do urllib, Python-urllib/3.x, com 403 Forbidden. Defina o seu próprio cabeçalho User-Agent e a mesma requisição retorna 200.
Posso fazer cache da resposta?
Sim, no seu próprio código. A API envia Cache-Control: private, no-store, então caches HTTP compartilhados não a armazenam. Nós reaproveitamos um veredito por uma hora para cada IP.
Isso vai bloquear os meus clientes reais?
Não, se você usar as quatro ações como foram pensadas. Um endereço do iCloud Private Relay voltou como allow, com pontuação 9, no nosso teste, e um servidor comum de nuvem voltou como allow, com 10. Só a saída Tor e uma VPN verificada por sondagem chegaram a block.
Preciso enviar dados pessoais?
Não. Você envia um endereço IP e recebe inteligência sobre esse endereço. Nomes, e-mails e dados de cartão ficam no seu sistema.
A equipe por trás da API do GeoIPHub
Escrito pela equipe do GeoIPHub, as pessoas que constroem a API de inteligência de IP do GeoIPHub e seu scanner. Os rascunhos são preparados com auxílio de IA e editados pela equipe; os exemplos de API são consultas reais à API em produção, salvo quando indicados como ilustrativos.


