How to Harden Signup & Login with an IP Geolocation API
Score new sign-ups and logins from one lookup — network type, anonymiser and threat signals — and step up verification only when risk warrants.
You can risk-score a sign-up or login inline — from a single IP lookup — and step up verification only when the signals warrant it. The response tells you whether a request comes from a datacentre network, hides behind VPN or Tor, or carries a known-threat flag. This guide turns that into an allow / email-verify / require-MFA decision, keeping friction off the 95% of genuine users.
The examples here are Python, standard library only — but the same logic maps directly onto our official SDKs for TypeScript, Go, Ruby, Java and 7 more. Field paths and the worked outputs below are all verified against the live API.
1Get a key and call the API securely
Create a free account here — 2,000 lookups/day, no card. Send the key as an Authorization: Bearer header from an environment variable, keeping it out of URLs and logs:
import os, urllib.request, json
API_KEY=*** # export it; never hardcode
def lookup(ip):
req = urllib.request.Request(
f"https://api.ipwho.org/ip/{ip}",
headers={"Authorization": f"Bearer {API_KEY}", "User-Agent": "MyApp/1.0"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())["data"]2Score the sign-up from the network and anonymiser signals:
Give a Corporate (cloud/hosting) network some weight — a lot of bulk
registration runs through datacentres — but don't over-trust it: the same
Corporate label also covers office networks, universities and
corporate VPNs that plenty of genuine users sign up from. So it should raise the
score toward a step-up check, never auto-reject. Keep it additive so no single
signal is a verdict:
def signup_risk(data):
conn = data.get("connection") or {}
sec = data.get("security") or {}
score, reasons = 0, []
# A Corporate network (cloud/hosting, but also offices & corporate VPNs) on a
# consumer signup is worth review weight — not proof. Step up, don't auto-reject.
if conn.get("connection_type") == "Corporate":
score += 40; reasons.append("datacentre/hosting network")
if sec.get("isTor"): score += 35; reasons.append("Tor")
if sec.get("isVpn"): score += 20; reasons.append("VPN")
threat = (sec.get("isThreat") or "low").lower()
if threat == "high": score += 30; reasons.append("high threat")
elif threat == "medium": score += 15; reasons.append("medium threat")
if score >= 60: action = "require email + MFA"
elif score >= 30: action = "email verify"
else: action = "allow"
return {"score": score, "action": action, "reasons": reasons}
print(signup_risk(lookup("185.220.101.1")))3Map the score to a step-up action — not a block:
Turn the score into allow, email verify, or require MFA. The goal is to add friction only where risk is real, never to hard-block a would-be customer. Feed this alongside your device and behavioural signals:
#!/usr/bin/env python3
"""Risk-score a signup/login from an IP (IPWho IP geolocation API).
Step up verification only when risk warrants — keep friction off genuine users."""
import os, sys, urllib.request, json
API_KEY=*** lookup(ip):
req = urllib.request.Request(
f"https://api.ipwho.org/ip/{ip}",
headers={"Authorization": f"Bearer {API_KEY}", "User-Agent": "IPWho-Signup/1.0"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())["data"]
def signup_risk(data):
conn = data.get("connection") or {}
sec = data.get("security") or {}
score, reasons = 0, []
if conn.get("connection_type") == "Corporate":
score += 40; reasons.append("datacentre/hosting network")
if sec.get("isTor"): score += 35; reasons.append("Tor")
if sec.get("isVpn"): score += 20; reasons.append("VPN")
threat = (sec.get("isThreat") or "low").lower()
if threat == "high": score += 30; reasons.append("high threat")
elif threat == "medium": score += 15; reasons.append("medium threat")
if score >= 60: action = "require email + MFA"
elif score >= 30: action = "email verify"
else: action = "allow"
return {"score": score, "action": action, "reasons": reasons}
if __name__ == "__main__":
ip = sys.argv[1] if len(sys.argv) > 1 else "185.220.101.1"
print(f"{ip}: {signup_risk(lookup(ip))}")$ python3 signup.py 185.220.101.1 # Tor exit node
185.220.101.1: {'score': 90, 'action': 'require email + MFA', 'reasons': ['datacentre/hosting network', 'Tor', 'medium threat']}
$ python3 signup.py 52.95.110.1 # AWS datacentre
52.95.110.1: {'score': 40, 'action': 'email verify', 'reasons': ['datacentre/hosting network']}
$ python3 signup.py 76.102.11.203 # residential broadband
76.102.11.203: {'score': 0, 'action': 'allow', 'reasons': []}