How to Detect VPN, Proxy & Tor Traffic with an IP Geolocation API
Flag anonymised and datacentre traffic from one lookup — VPN, Tor, and hosting networks — then challenge rather than hard-block.
You can flag anonymised traffic — VPN, Tor, and datacentre/hosting networks — from a single IP lookup. The response carries security.isVpn, security.isTor, a threat level, and the connection type. This guide combines them into a verdict you can act on — and the honest rule is to challenge, not hard-block, because plenty of legitimate users browse via VPN.
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"]2Four fields reveal anonymised traffic:
Read security.isVpn and security.isTor (booleans), security.isThreat (a string level), and connection.connection_type — where Corporate flags cloud, hosting and datacentre networks (it also includes offices and corporate VPNs, so treat it as a hint, not a hard tell):
data = lookup("185.220.101.1") # a real Tor exit node
sec = data.get("security") or {}
conn = data.get("connection") or {}
is_vpn = sec.get("isVpn") # bool
is_tor = sec.get("isTor") # bool -> True here
threat = sec.get("isThreat") # "low" | "medium" | "high"
ctype = conn.get("connection_type") # "Corporate" = cloud/hosting/business
print(is_vpn, is_tor, threat, ctype) # False True medium Corporate3Combine into a verdict — challenge, don't block:
Fold the flags into an action. Reserve a hard block for a known-bad combination (e.g. a high-threat Tor exit); otherwise challenge with MFA or a captcha. Legitimate customers use VPNs — let them prove themselves:
#!/usr/bin/env python3
"""Detect VPN/proxy/Tor/datacentre traffic (IPWho IP geolocation API).
Challenge, don't hard-block — legitimate users use VPNs too."""
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-Anon/1.0"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())["data"]
def anonymiser(data):
sec = data.get("security") or {}
conn = data.get("connection") or {}
flags = []
if sec.get("isTor"): flags.append("tor")
if sec.get("isVpn"): flags.append("vpn")
if conn.get("connection_type") == "Corporate": flags.append("datacenter")
threat = (sec.get("isThreat") or "low").lower()
if "tor" in flags and threat == "high":
verdict = "block" # known-bad anonymiser
elif flags:
verdict = "challenge" # step-up: MFA / captcha
else:
verdict = "allow"
return {"flags": flags or ["clean"], "threat": threat, "verdict": verdict}
if __name__ == "__main__":
ip = sys.argv[1] if len(sys.argv) > 1 else "185.220.101.1"
print(f"{ip}: {anonymiser(lookup(ip))}")$ python3 anon.py 185.220.101.1 # Tor exit node
185.220.101.1: {'flags': ['tor', 'datacenter'], 'threat': 'medium', 'verdict': 'challenge'}
$ python3 anon.py 52.95.110.1 # AWS datacentre
52.95.110.1: {'flags': ['datacenter'], 'threat': 'low', 'verdict': 'challenge'}
$ python3 anon.py 76.102.11.203 # residential broadband
76.102.11.203: {'flags': ['clean'], 'threat': 'low', 'verdict': 'allow'}