API de detección de fraude por IP: puntúa registros, inicios de sesión y checkouts en Node y Python
Añade una API de detección de fraude por IP al registro, el inicio de sesión y el checkout: código en Node y Python que ejecutamos sobre seis IP públicas, con salida real, caché y los límites de tasa.

Llegan dos registros con un segundo de diferencia, y ambos ocultan la dirección doméstica de la persona. Uno viene de una salida de Tor en Hamburgo. El otro viene de iCloud Private Relay, que Apple describe como "parte de una suscripción a iCloud+" que te protege "cuando navegas por la web en Safari" (Soporte de Apple). Una regla que bloquea "todo lo oculto" rechaza a ambos, y uno de ellos es un cliente de Apple que paga.
Distinguirlos es el trabajo. Es una parte de lo que abarca la inteligencia de IP, y cabe en un solo archivo de menos de 50 líneas. A continuación tienes la petición, una respuesta real, una versión en Node y otra en Python con caché y gestión de fallos, lo que imprimieron para seis direcciones y dónde se queda corta la comprobación.

La única petición que necesita una API de detección de fraude por IP
Toda la integración es un único GET con la IP en la ruta y tu clave en la cabecera x-api-key. La referencia de la API documenta la misma forma:
curl https://api.geoiphub.com/v1/lookup/45.83.220.7 \
-H "x-api-key: YOUR_API_KEY"
Tres detalles pillan a la gente en el primer intento:
- La IP va en la ruta.
/v1/lookup/{ip}es la única forma. Una versión con query string redirige en lugar de devolver datos. - No envíes el marcador de posición. Enviamos la cadena literal
YOUR_API_KEYy recibimos401con{"message":"Invalid API key"}. Omite la cabecera hasta que tengas una clave real; las llamadas anónimas funcionan a 30 por minuto. - Una IP por llamada. Cada petición admite una dirección, así que un script que revisa una lista hace una petición por dirección y cada una cuenta para tu límite.
Crea una clave gratuita en el panel cuando pases de las pruebas. El plan gratuito da a esa clave 1.500 consultas al día, con todos los campos incluidos.
Qué devolvió una dirección de Mullvad y sobre qué campos ramificar
La respuesta ocupa unos 2,7 KB de JSON anidado en diez grupos, y una comprobación de fraude necesita dos de ellos: detection y scoring. Esta es la consulta de 45.83.220.7, recortada pero con los 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"
}
}
Esta dirección está en 31173 Services, la red en la que Mullvad VPN tiene sus servidores, en Malmö. Cuando lo comprobamos el 13 de octubre de 2026, no figuraba en la lista de servidores que publica la propia Mullvad. Se activaron dos señales. known_vpn_server procede de nuestra tabla de redes de hosting de VPN conocidas, y por eso vpn_confidence es 85 y no el 95 que da una coincidencia con la lista de servidores de la propia Mullvad. datacenter_ip indica que es hosting, no una conexión doméstica. Juntas suman 46, que cae en review: una VPN por sí sola no es fraude.
Tres campos sostienen la decisión:
| Campo | Qué contiene | Cómo lo usa el código |
|---|---|---|
scoring.recommended_action | allow (puntuación hasta 25), review (hasta 50), step_up (hasta 75), block (más de 75) | La rama |
scoring.fraud_score | La puntuación compuesta de 0 a 100 que hay detrás de esa acción | Registro y ajuste posterior |
scoring.detection_methods | Las señales que se activaron, como tor_exit u openvpn_open | El motivo que guardas y muestras a los revisores |
Los indicadores de detection (is_vpn, is_proxy, is_tor, is_hosting, is_relay) son los hechos en bruto que hay detrás de la puntuación. Úsalos para las pocas reglas en las que tu negocio se aparta de las bandas predeterminadas. La puntuación también tiene señales exculpatorias: un ISP residencial resta 20 puntos y un rastreador verificado 30, así que un indicador por sí solo no empuja una dirección a block. La guía de la puntuación de fraude de IP explica qué mueve la cifra.
La versión en Node: fetch, una caché de una hora y cuatro ramas
Este es el archivo exacto que ejecutamos, en Node 24 y sin dependencias. Necesita Node 18 o posterior para el fetch integrado:
// 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);
}
Cuatro decisiones del código son deliberadas:
- Parte de
recommended_actiony solo lo sobrescribe en dos sitios. Una salida de Tor siempre se bloquea, y en el checkoutreviewpasa a serstep_up, porque es ahí donde sale el dinero. - Falla en suave. Un tiempo de espera agotado, un 5xx o un 429 devuelven
review, noblock. Si nuestra API no está disponible, tus registros siguen funcionando y quedan registrados para una segunda revisión. - El tiempo de espera es de 3 segundos. Por nuestra parte, una IP conocida se responde desde una base de datos mapeada en memoria en menos de un milisegundo, así que el viaje de ida y vuelta por la red es casi toda la espera. Una IP que no hemos visto nunca se clasifica en vivo en menos de 2,5 segundos, y 3 segundos cubren ese caso.
- La clave se queda en tu servidor. Llama a la API desde tu backend, nunca desde código del navegador, donde cualquiera puede leer la clave.
La versión en Python necesita un User-Agent o recibe un 403
Nuestra API responde con 403 Forbidden al User-Agent predeterminado de urllib en Python, Python-urllib/3.x. Lo reprodujimos el 16 de octubre de 2026 a las 05:36 UTC con una llamada simple a urllib.request.urlopen: HTTP Error 403: Forbidden. Curl y requests no tienen este problema, porque envían User-Agents distintos. Así que el archivo de Python establece el suyo, con un comentario que explica por qué:
# 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", ""))
Es la misma lógica que el archivo de Node, usando solo la biblioteca estándar, probada en Python 3.9.6. urllib lanza HTTPError para cualquier estado que no sea 2xx, así que el 429 y su cabecera Retry-After se leen de la excepción.
Lo que imprimieron ambos scripts para seis direcciones públicas
Ambos scripts dieron el mismo veredicto para todas las direcciones. Ejecutamos el archivo de Node a las 07:39 UTC y el de Python a las 07:40 UTC del 16 de octubre de 2026, sin clave, contra seis direcciones públicas. El último comando vuelve a ejecutar la dirección de 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 con los mismos argumentos imprimió las mismas siete líneas, carácter por carácter. Esto es cada dirección y por qué acabó donde acabó:
| Dirección | Qué es | Acción | Señales que se activaron |
|---|---|---|---|
45.33.32.156 | scanme.nmap.org, el host público de pruebas de Nmap en Linode | allow, 10 | Solo datacenter_ip. El hosting por sí solo no es fraude |
104.28.46.99 | Salida de iCloud Private Relay en Cloudflare | allow, 9 | relay_ip. La puntuación tiene un tope de 60 para las direcciones de relé, así que nunca pueden llegar a block |
45.83.220.7 | La red de Mullvad en Malmö | review, 46; step_up en el checkout | known_vpn_server, datacenter_ip |
91.90.122.25 | Servidor de PIA VPN en Schaan, Liechtenstein, en M247 | step_up, 64 | datacenter_blocklisted, no_ptr_datacenter, known_spammer. No se marca como VPN |
102.129.235.17 | Servidor de VPN comercial en Washington D. C. | block, 92 | known_vpn_server, más openvpn_open e ipsec_open: nuestras sondas completaron handshakes de OpenVPN e IPsec |
185.220.101.1 | Salida de Tor en Hamburgo | block, 100 | tor_exit, datacenter_blocklisted, no_ptr_datacenter, known_spammer |
Los dos registros del principio acabaron en extremos opuestos: la salida de Tor con 100 y el usuario de Private Relay con 9. Esa dispersión es la razón para puntuar en lugar de bloquear toda dirección oculta.
Los resultados cambian a medida que cambian nuestros datos. Hasta esta reescritura, este artículo mostraba 45.83.220.7 como hosting normal con una puntuación de 10 y allow. Para el 8 de octubre de 2026 nuestro escáner la había vinculado a Mullvad, y ahora obtiene 46. Registra la acción y los métodos con cada decisión, para que un cambio así aparezca en tus datos en lugar de sorprenderte.
Dónde llamarla: en cada registro, inicio de sesión y checkout
Puntúa cada registro, cada inicio de sesión y cada checkout, en el servidor, antes de confirmar la acción. Son los tres momentos en los que un estafador obtiene algo, y son lo bastante poco frecuentes por usuario como para caber en un presupuesto diario. No la llames en cada vista de página.
Lo que significa cada acción en la práctica:
| Acción | Registro | Inicio de sesión | Checkout |
|---|---|---|---|
allow | Crear la cuenta | Iniciar sesión | Cobrar el pago |
review | Crearla y marcarla para revisión | Iniciar sesión y registrar el motivo | Nuestro código la sube a step_up |
step_up | Verificar antes el email o el teléfono | Pedir el segundo factor | Ejecutar 3-D Secure |
block | Rechazar con un mensaje neutro | Rechazar y contarlo para el bloqueo de la cuenta | Rechazar el pedido |
Las bandas predeterminadas son un punto de partida, y tus propios datos de fraude deberían moverlas. Nuestros umbrales de puntuación de fraude para inicio de sesión, registro y checkout dan puntos de corte iniciales por acción y el razonamiento de cada uno.
Puntúa también la dirección correcta. Detrás de un balanceador de carga o una CDN, la dirección del socket es tu proxy, y la IP del cliente está en una cabecera de reenvío que cualquier cliente también puede falsificar. Tómala del salto que añadió tu propio proxy de confianza, nunca del valor más a la izquierda que envió un cliente.
Mantenerse por debajo de 1.500 consultas al día: caché y límite de tasa
El plan gratuito permite 1.500 consultas al día. Guarda en caché por IP en tu propio código y el presupuesto da para mucho más, porque una persona normalmente se registra, inicia sesión y paga desde la misma dirección en menos de una hora.
La API en sí no ayuda con la caché: cada respuesta lleva Cache-Control: private, no-store, así que ninguna caché HTTP compartida la guardará. Ambos scripts conservan un veredicto durante una hora en la memoria del proceso. Usa Redis o tu caché existente si tienes más de un servidor. No conserves un veredicto más de un día, porque nuestras fuentes de amenazas y la base de datos de consultas se actualizan a diario.
Los dos límites se comportan de forma distinta, y llegamos a uno de ellos mientras probábamos este artículo:
| Llamada como | Límite | Qué pasa al superarlo |
|---|---|---|
| Sin clave | 30 consultas por minuto por IP de origen | La petición número 30 dentro de un minuto devuelve 429 y bloquea esa IP durante dos horas |
| Clave gratuita | 1.500 consultas al día | 429 con una cabecera Retry-After hasta que se reinicia el contador diario |
A las 05:37 UTC del 16 de octubre de 2026 lanzamos una ráfaga de peticiones anónimas para ver el 429. En veinte segundos la API devolvió 429 con Retry-After: 7199, y todas las llamadas anónimas desde nuestra IP quedaron bloqueadas durante dos horas, incluidos nuestros propios scripts. Su ruta de fallo suave es la primera salida real que obtuvimos:
45.33.32.156 review - - lookup failed: rate limited, retry after 6971s
45.33.32.156 review - - HTTP 429, retry after 6970s
Así que trata el límite anónimo como un margen para pruebas y usa una clave en producción. Ramifica según el código de estado y Retry-After, no según el texto del mensaje del 429.
Dónde se queda corta esta comprobación
Un veredicto de IP es una señal, y se le escapan cosas. Mientras escribíamos este artículo surgieron cuatro casos:
- Un servidor de VPN que figura en una lista puede volver limpio. El servidor de PIA
91.90.122.25estaba en la lista que publica la propia PIA el 4 de octubre de 2026, pero nuestra API devuelveis_vpn: false. Llegó astep_upsolo porque está en una lista de bloqueo DNS. Sin ese historial habría pasado como hosting normal. - Los proxies residenciales son en su mayoría invisibles para los datos de IP. El tráfico enrutado a través de conexiones domésticas reales parece de un usuario doméstico. El campo
is_residential_proxyes una inferencia aproximada a partir de la red, no una detección, así que no construyas una regla sobre él. - Las direcciones compartidas arrastran el historial de otras personas. Los operadores móviles y el CGNAT colocan a muchos usuarios detrás de una IP, y por eso la puntuación tiene un tope para los rangos de CGNAT y
blockdebería seguir siendo poco frecuente ahí. - Las ubicaciones de los relés son aproximadas. Para
104.28.46.99, nuestra geolocalización dice Luanda, Angola, mientras que la lista de salidas de la propia Apple sitúa ese rango en Lusaka, Zambia. Usa el indicador de relé, no la ciudad, para tomar decisiones, como explica nuestra guía para dejar pasar Private Relay.
Combina la comprobación de IP con lo que sabes de la cuenta: antigüedad del email, dispositivo, historial de pagos y velocidad. La guía del stack de detección de fraude en pagos muestra dónde encajan las señales de IP entre ellas.
Pruébala primero con tu propio tráfico
Antes de integrarla en un flujo en producción, ejecuta cualquiera de los dos scripts sobre un día de IP de registros pasados con una clave gratuita y compara sus acciones con lo que ya sabes de esas cuentas. Si block habría detectado cuentas que luego vetaste, y allow cubre a los clientes que conservaste, ponla primero delante del registro, después del inicio de sesión y después del checkout. Para comprobar una dirección a mano, el comprobador de puntuación de fraude de IP muestra la misma puntuación y los mismos motivos sin escribir código.
Preguntas frecuentes
¿La API de GeoIPHub es gratuita para empezar?
Sí. El plan gratuito incluye 1.500 consultas al día con todos los campos de datos, una clave de API y sin tarjeta de crédito. Sin clave puedes hacer 30 consultas por minuto para pruebas.
¿Sobre qué campo debe ramificar mi código?
scoring.recommended_action. Es allow hasta una puntuación de 25, review hasta 50, step_up hasta 75 y block por encima de 75. Lee scoring.detection_methods para registrar el motivo.
¿Por qué mi script de Python recibe un 403?
Nuestra API responde con 403 Forbidden al User-Agent predeterminado de urllib, Python-urllib/3.x. Establece tu propia cabecera User-Agent y la misma petición devuelve 200.
¿Puedo guardar la respuesta en caché?
Sí, en tu propio código. La API envía Cache-Control: private, no-store, así que las cachés HTTP compartidas no la guardarán. Nosotros reutilizamos un veredicto durante una hora por IP.
¿Bloqueará a mis clientes reales?
No si usas las cuatro acciones tal como están diseñadas. Una dirección de iCloud Private Relay volvió como allow con una puntuación de 9 en nuestra prueba, y un servidor en la nube corriente volvió como allow con 10. Solo la salida de Tor y una VPN verificada por sondeo llegaron a block.
¿Tengo que enviar datos personales?
No. Envías una dirección IP y obtienes inteligencia sobre esa dirección. Los nombres, los emails y los datos de tarjeta se quedan en tu sistema.
El equipo detrás de la API de GeoIPHub
Escrito por el equipo de GeoIPHub, las personas que crean la API de inteligencia IP de GeoIPHub y su escáner. Los borradores se preparan con ayuda de IA y el equipo los edita; los ejemplos de la API son consultas reales contra la API en producción, salvo que se indique que son ilustrativos.


