API de détection de fraude IP : évaluer les inscriptions, connexions et checkouts en Node et Python
Ajoutez une API de détection de fraude IP à l'inscription, à la connexion et au checkout : du code Node et Python exécuté sur six IP publiques, avec la sortie réelle, le cache et les limites de débit.

Deux inscriptions arrivent à une seconde d'intervalle, et toutes deux cachent l'adresse personnelle de l'utilisateur. L'une provient d'une sortie Tor à Hambourg. L'autre provient d'iCloud Private Relay, qu'Apple décrit comme « inclus dans un abonnement iCloud+ » et qui vous protège « lorsque vous naviguez sur le web dans Safari » (Assistance Apple). Une règle qui bloque « tout ce qui est masqué » rejette les deux, et l'une d'elles est une cliente Apple payante.
Distinguer ces deux cas, c'est tout le travail. C'est une partie de ce que couvre l'intelligence IP, et cela tient dans un seul fichier de moins de 50 lignes. Vous trouverez ci-dessous la requête, une réponse réelle, une version Node et une version Python avec cache et gestion des échecs, ce qu'elles ont affiché pour six adresses, et les limites de cette vérification.

La seule requête dont une API de détection de fraude IP a besoin
Toute l'intégration se résume à un GET avec l'IP dans le chemin et votre clé dans l'en-tête x-api-key. La référence de l'API documente la même structure :
curl https://api.geoiphub.com/v1/lookup/45.83.220.7 \
-H "x-api-key: YOUR_API_KEY"
Trois détails piègent les gens au premier essai :
- L'IP va dans le chemin.
/v1/lookup/{ip}est la seule forme possible. Une version avec paramètre de requête redirige au lieu de renvoyer des données. - N'envoyez pas l'espace réservé. Nous avons envoyé la chaîne littérale
YOUR_API_KEYet obtenu401avec{"message":"Invalid API key"}. Omettez l'en-tête tant que vous n'avez pas de vraie clé ; les appels anonymes fonctionnent à raison de 30 par minute. - Une IP par appel. Chaque requête porte sur une seule adresse : un script qui filtre une liste effectue donc une requête par adresse, et chacune est décomptée de votre limite.
Créez une clé gratuite dans le tableau de bord lorsque vous dépassez le stade des tests. L'offre gratuite donne à cette clé 1 500 requêtes par jour, avec tous les champs inclus.
Ce qui est revenu pour une adresse Mullvad, et sur quels champs s'appuyer
La réponse fait environ 2,7 Ko de JSON imbriqué en dix groupes, et une vérification de fraude n'a besoin que de deux d'entre eux : detection et scoring. Voici la requête pour 45.83.220.7, abrégée mais avec les valeurs intactes :
{
"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"
}
}
Cette adresse se trouve sur 31173 Services, le réseau sur lequel Mullvad VPN fait tourner ses serveurs, à Malmö. Lorsque nous avons vérifié le 13 octobre, elle ne figurait pas sur la liste de serveurs publiée par Mullvad. Deux signaux se sont déclenchés. known_vpn_server provient de notre table des réseaux d'hébergement VPN connus, ce qui explique pourquoi vpn_confidence vaut 85 et non 95, valeur obtenue en cas de correspondance avec la liste de serveurs de Mullvad elle-même. datacenter_ip indique qu'il s'agit d'hébergement, pas d'une connexion domestique. Ensemble, ils donnent un score de 46, qui tombe dans review : un VPN à lui seul n'est pas une fraude.
Trois champs portent la décision :
| Champ | Ce qu'il contient | Comment le code l'utilise |
|---|---|---|
scoring.recommended_action | allow (score jusqu'à 25), review (jusqu'à 50), step_up (jusqu'à 75), block (au-dessus de 75) | Le branchement |
scoring.fraud_score | Le score composite de 0 à 100 derrière cette action | Journalisation et ajustements ultérieurs |
scoring.detection_methods | Les signaux déclenchés, comme tor_exit ou openvpn_open | La raison que vous stockez et montrez aux personnes chargées de l'examen |
Les indicateurs de detection (is_vpn, is_proxy, is_tor, is_hosting, is_relay) sont les faits bruts derrière le score. Utilisez-les pour les quelques règles où votre activité s'écarte des tranches par défaut. Le score comporte aussi des signaux disculpants : un FAI résidentiel retire 20 points et un robot d'exploration vérifié 30, donc un indicateur seul ne fait pas basculer une adresse dans block. Le guide du score de fraude IP explique ce qui fait varier le chiffre.
La version Node : fetch, un cache d'une heure et quatre branches
Voici le fichier exact que nous avons exécuté, sous Node 24, sans dépendance. Il nécessite Node 18 ou plus récent pour le fetch intégré :
// 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);
}
Quatre choix y sont délibérés :
- Le code part de
recommended_actionet ne le remplace qu'à deux endroits. Une sortie Tor est toujours bloquée, et au checkoutreviewdevientstep_up, car c'est là que l'argent sort. - Il échoue en douceur. Un délai dépassé, une erreur 5xx ou une 429 renvoie
review, pasblock. Si notre API est injoignable, vos inscriptions continuent de fonctionner et sont journalisées pour un second examen. - Le délai d'attente est de 3 secondes. De notre côté, une IP connue reçoit une réponse depuis une base de données mappée en mémoire en moins d'une milliseconde : l'aller-retour réseau représente donc l'essentiel de l'attente. Une IP que nous n'avons jamais vue est classée en direct en moins de 2,5 secondes, et 3 secondes couvrent ce cas.
- La clé reste sur votre serveur. Appelez l'API depuis votre backend, jamais depuis du code exécuté dans le navigateur, où n'importe qui peut lire la clé.
La version Python a besoin d'un User-Agent, sinon elle reçoit une 403
Notre API répond au User-Agent par défaut d'urllib en Python, Python-urllib/3.x, par 403 Forbidden. Nous l'avons reproduit le 16 octobre à 05:36 UTC avec un simple appel urllib.request.urlopen : HTTP Error 403: Forbidden. Curl et requests ne sont pas concernés, car ils envoient d'autres User-Agents. Le fichier Python définit donc le sien, avec un commentaire qui explique pourquoi :
# 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", ""))
C'est la même logique que le fichier Node, avec uniquement la bibliothèque standard, testée sous Python 3.9.6. urllib lève HTTPError pour tout statut hors 2xx, donc la 429 et son en-tête Retry-After sont lus depuis l'exception.
Ce que les deux scripts ont affiché pour six adresses publiques
Les deux scripts ont rendu le même verdict pour chaque adresse. Nous avons exécuté le fichier Node à 07:39 UTC et le fichier Python à 07:40 UTC le 16 octobre 2026, sans clé, sur six adresses publiques. La dernière commande relance l'adresse Mullvad dans le cadre d'un 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 avec les mêmes arguments a affiché les mêmes sept lignes, caractère pour caractère. Voici ce qu'est chaque adresse et pourquoi elle a atterri là où elle a atterri :
| Adresse | Ce que c'est | Action | Signaux déclenchés |
|---|---|---|---|
45.33.32.156 | scanme.nmap.org, l'hôte de test public de Nmap chez Linode | allow, 10 | datacenter_ip uniquement. L'hébergement seul n'est pas une fraude |
104.28.46.99 | Sortie iCloud Private Relay sur Cloudflare | allow, 9 | relay_ip. Le score plafonne les adresses de relais à 60 : elles ne peuvent donc jamais atteindre block |
45.83.220.7 | Le réseau de Mullvad à Malmö | review, 46 ; step_up au checkout | known_vpn_server, datacenter_ip |
91.90.122.25 | Serveur VPN PIA à Schaan, au Liechtenstein, chez M247 | step_up, 64 | datacenter_blocklisted, no_ptr_datacenter, known_spammer. Non signalé comme VPN |
102.129.235.17 | Serveur VPN commercial à Washington, D.C. | block, 92 | known_vpn_server, plus openvpn_open et ipsec_open : nos sondes ont mené à bien des handshakes OpenVPN et IPsec |
185.220.101.1 | Sortie Tor à Hambourg | block, 100 | tor_exit, datacenter_blocklisted, no_ptr_datacenter, known_spammer |
Les deux inscriptions de l'introduction se sont retrouvées aux deux extrémités : la sortie Tor à 100 et l'utilisatrice de Private Relay à 9. Cet écart est la raison pour laquelle il faut attribuer un score plutôt que bloquer toute adresse masquée.
Les résultats évoluent à mesure que nos données changent. Jusqu'à cette réécriture, cet article présentait 45.83.220.7 comme un simple hébergement avec un score de 10 et allow. Au 8 octobre, notre scanner l'avait rattachée à Mullvad, et elle obtient désormais 46. Journalisez l'action et les méthodes avec chaque décision, pour qu'un changement de ce type apparaisse dans vos données au lieu de vous surprendre.
Où l'appeler : chaque inscription, connexion et checkout
Évaluez chaque inscription, chaque connexion et chaque checkout, côté serveur, avant de valider l'action. Ce sont les trois moments où un fraudeur gagne quelque chose, et ils sont assez rares par utilisateur pour tenir dans un budget quotidien. Ne l'appelez pas à chaque affichage de page.
Ce que chaque action signifie en pratique :
| Action | Inscription | Connexion | Checkout |
|---|---|---|---|
allow | Créer le compte | Connecter l'utilisateur | Encaisser le paiement |
review | Le créer, le signaler pour examen | Connecter l'utilisateur, journaliser la raison | Notre code le fait passer à step_up |
step_up | Vérifier d'abord l'e-mail ou le téléphone | Demander le second facteur | Lancer 3-D Secure |
block | Refuser avec un message neutre | Refuser, et le compter pour le verrouillage | Refuser la commande |
Les tranches par défaut sont un point de départ, et vos propres données de fraude doivent les faire évoluer. Nos seuils de score de fraude pour la connexion, l'inscription et le checkout proposent des seuils de départ par action et le raisonnement derrière chacun.
Évaluez aussi la bonne adresse. Derrière un répartiteur de charge ou un CDN, l'adresse de la socket est celle de votre proxy, et l'IP du client se trouve dans un en-tête de transfert que n'importe quel client peut aussi falsifier. Prenez-la au niveau du saut ajouté par votre propre proxy de confiance, jamais à partir de la valeur la plus à gauche envoyée par un client.
Rester sous les 1 500 requêtes par jour : cache et limite de débit
L'offre gratuite autorise 1 500 requêtes par jour. Mettez en cache par IP dans votre propre code et le budget va beaucoup plus loin, car une même personne s'inscrit, se connecte et paie généralement depuis la même adresse en l'espace d'une heure.
L'API elle-même ne vous aidera pas pour le cache : chaque réponse porte Cache-Control: private, no-store, donc aucun cache HTTP partagé ne la conservera. Les deux scripts conservent un verdict pendant une heure en mémoire du processus. Utilisez Redis ou votre cache existant si vous faites tourner plus d'un serveur. Ne conservez pas un verdict plus d'une journée, car nos flux de menaces et la base de recherche sont actualisés chaque jour.
Les deux limites se comportent différemment, et nous avons atteint l'une d'elles en testant cet article :
| Appel effectué | Limite | Ce qui se passe au-delà |
|---|---|---|
| Sans clé | 30 requêtes par minute par IP appelante | La 30e requête dans la même minute renvoie 429 et bloque cette IP pendant deux heures |
| Clé gratuite | 1 500 requêtes par jour | 429 avec un en-tête Retry-After jusqu'à la réinitialisation du compteur quotidien |
Le 16 octobre à 05:37 UTC, nous avons envoyé une rafale de requêtes anonymes pour voir la 429. En vingt secondes, l'API a renvoyé 429 avec Retry-After: 7199, et tous les appels anonymes depuis notre IP sont restés bloqués pendant deux heures, y compris ceux de nos propres scripts. Leur chemin d'échec en douceur est la première sortie réelle que nous avons obtenue :
45.33.32.156 review - - lookup failed: rate limited, retry after 6971s
45.33.32.156 review - - HTTP 429, retry after 6970s
Considérez donc la limite anonyme comme une tolérance pour les tests, et utilisez une clé en production. Basez votre logique sur le code de statut et Retry-After, pas sur le texte du message de la 429.
Les limites de cette vérification
Un verdict IP est un signal parmi d'autres, et il passe à côté de certaines choses. Quatre cas sont apparus pendant la rédaction de cet article :
- Un serveur VPN listé peut revenir propre. Le serveur PIA
91.90.122.25figurait sur la liste publiée par PIA le 4 octobre, et pourtant notre API renvoieis_vpn: false. Il n'a atteintstep_upque parce qu'il figure sur une liste de blocage DNS. Sans cet historique, il serait passé pour un simple hébergement. - Les proxys résidentiels sont en grande partie invisibles pour les données IP. Le trafic acheminé via de vraies connexions domestiques ressemble à celui d'un utilisateur à domicile. Le champ
is_residential_proxyest une inférence grossière à partir du réseau, pas une détection : ne construisez donc pas de règle dessus. - Les adresses partagées portent l'historique d'autres personnes. Les opérateurs mobiles et le CGNAT placent de nombreux utilisateurs derrière une seule IP, ce qui explique pourquoi le score plafonne les plages CGNAT et pourquoi
blockdoit y rester rare. - Les localisations des relais sont approximatives. Pour
104.28.46.99, notre géolocalisation indique Luanda, en Angola, alors que la liste de sorties d'Apple place cette plage à Lusaka, en Zambie. Utilisez l'indicateur de relais, pas la ville, pour vos décisions, comme l'explique notre guide pour laisser passer Private Relay.
Associez la vérification IP à ce que vous savez du compte : ancienneté de l'e-mail, appareil, historique de paiement et vélocité. Le guide de la pile de détection de la fraude au paiement montre où se situent les signaux IP parmi ces éléments.
Testez-la d'abord sur votre propre trafic
Avant de l'intégrer à un parcours en production, exécutez l'un ou l'autre script sur une journée d'IP d'inscriptions passées avec une clé gratuite, et comparez ses actions à ce que vous savez déjà de ces comptes. Si block aurait attrapé des comptes que vous avez bannis par la suite, et si allow couvre les clients que vous avez gardés, placez-la d'abord devant l'inscription, puis la connexion, puis le checkout. Pour vérifier une adresse à la main, le vérificateur de score de fraude IP affiche le même score et les mêmes raisons, sans aucun code.
Questions fréquentes
L'API GeoIPHub est-elle gratuite pour commencer ?
Oui. L'offre gratuite comprend 1 500 requêtes par jour avec tous les champs de données, une clé API et aucune carte bancaire. Sans clé, vous pouvez effectuer 30 requêtes par minute pour vos tests.
Sur quel champ mon code doit-il s'appuyer ?
scoring.recommended_action. Il vaut allow jusqu'à un score de 25, review jusqu'à 50, step_up jusqu'à 75 et block au-dessus de 75. Lisez scoring.detection_methods pour journaliser la raison.
Pourquoi mon script Python reçoit-il une erreur 403 ?
Notre API répond au User-Agent par défaut d'urllib, Python-urllib/3.x, par 403 Forbidden. Définissez votre propre en-tête User-Agent et la même requête renvoie 200.
Puis-je mettre la réponse en cache ?
Oui, dans votre propre code. L'API envoie Cache-Control: private, no-store, donc les caches HTTP partagés ne la conserveront pas. Nous réutilisons un verdict pendant une heure par IP.
Va-t-elle bloquer mes vrais clients ?
Pas si vous utilisez les quatre actions comme prévu. Lors de notre test, une adresse iCloud Private Relay est revenue allow avec un score de 9, et un simple serveur cloud allow avec 10. Seuls la sortie Tor et un VPN vérifié par sondage ont atteint block.
Dois-je envoyer des données personnelles ?
Non. Vous envoyez une adresse IP et obtenez des informations sur cette adresse. Les noms, e-mails et données de carte restent dans votre système.
L'équipe derrière l'API GeoIPHub
Rédigé par l'équipe GeoIPHub, les personnes qui développent l'API de renseignement IP GeoIPHub et son scanner. Les brouillons sont préparés avec l'aide de l'IA et relus par l'équipe ; les exemples d'API sont de vraies requêtes sur l'API en production, sauf mention « illustratif ».


