How to Geo-Gate Content by IP (with Spoof Detection)
Enforce licensed regions and blackouts from one lookup — and catch VPN/Tor geo-spoofing in the same call.
You can enforce licensing regions and blackouts and catch the VPN/Tor spoofing used to evade them — from a single IP lookup. The response gives you country for the allow/deny decision and security.isVpn / isTor to flag an anonymiser pretending to be in-region. This guide combines both into one gate.
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"]2Decide allow/deny by country, catch spoofing in the same call:
Compare geoLocation.countryCode against your licensed set, and reject the request if security.isVpn or security.isTor is set — an anonymiser claiming to be in-region is the classic way users evade a geo-block. This catches the common cases, not every one: a determined user on a residential proxy can still slip through, so pair this gate with account-level entitlements for high-value rights:
LICENSED = {"US", "CA", "GB"} # regions you hold rights for
def geo_gate(data):
geo = data.get("geoLocation") or {}
sec = data.get("security") or {}
cc = geo.get("countryCode")
# An anonymiser in-region is the classic licensing-evasion move
spoof = bool(sec.get("isVpn") or sec.get("isTor"))
allowed = (cc in LICENSED) and not spoof
if spoof: reason = "vpn/tor geo-spoof"
elif cc in LICENSED: reason = "licensed region"
else: reason = "outside licensed region"
return {"country": cc, "allowed": allowed, "reason": reason}
print(geo_gate(lookup("76.102.11.203")))3Enforce the gate — the full script:
Everything together: a region allow-list plus spoof detection in one lookup. Tune the LICENSED set to your rights, and remember IP geo is approximate — pair it with account-level entitlements for anything high-stakes:
#!/usr/bin/env python3
"""Geo-gate content by IP with spoof detection (IPWho IP geolocation API)."""
import os, sys, urllib.request, json
API_KEY=*** LICENSED = {"US", "CA", "GB"}
def lookup(ip):
req = urllib.request.Request(
f"https://api.ipwho.org/ip/{ip}",
headers={"Authorization": f"Bearer {API_KEY}", "User-Agent": "IPWho-GeoGate/1.0"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())["data"]
def geo_gate(data):
geo = data.get("geoLocation") or {}
sec = data.get("security") or {}
cc = geo.get("countryCode")
spoof = bool(sec.get("isVpn") or sec.get("isTor"))
allowed = (cc in LICENSED) and not spoof
if spoof: reason = "vpn/tor geo-spoof"
elif cc in LICENSED: reason = "licensed region"
else: reason = "outside licensed region"
return {"country": cc, "allowed": allowed, "reason": reason}
if __name__ == "__main__":
ip = sys.argv[1] if len(sys.argv) > 1 else "76.102.11.203"
print(f"{ip}: {geo_gate(lookup(ip))}")$ python3 geogate.py 76.102.11.203 # USA, residential -> allowed
76.102.11.203: {'country': 'US', 'allowed': True, 'reason': 'licensed region'}
$ python3 geogate.py 91.0.0.1 # Germany -> outside licensed region
91.0.0.1: {'country': 'DE', 'allowed': False, 'reason': 'outside licensed region'}
$ python3 geogate.py 185.220.101.1 # Tor exit -> spoof blocked
185.220.101.1: {'country': 'DE', 'allowed': False, 'reason': 'vpn/tor geo-spoof'}