IPWho (ipwho.org) Python SDK
Official Python SDK for the IPWho IP geolocation API — geoip lookup, IP location, IP to country / latitude / longitude, ASN/ISP, timezone, currency, flag, and proxy/VPN detection with typed responses. Works as an IP lookup / ip-geolocation client for IPv4 and IPv6 (lookup, me, bulk).
- Product: ipwho.org
- API docs: ipwho.org/docs
- Get an API key: ipwho.org/free-plan (free Lavrox account)
- Live API host:
https://api.ipwho.org
API key
Open a free Lavrox account to get an API key for IPWho. Create your key at ipwho.org/free-plan — no credit card required.
Installation
pip install ipwho-ip-geolocation-api
From source:
git clone https://github.com/lavrox/SDK-IPWho-Python.git
cd SDK-IPWho-Python
pip install -e .
Requires Python 3.8+ and requests.
Quick Start
import os
from ipwho import IPWhoClient
client = IPWhoClient(api_key=os.environ["IPWHO_API_KEY"])
resp = client.lookup("8.8.8.8") # GET /ip/{ip}
me = client.me() # GET /me
bulk = client.bulk(["8.8.8.8", "1.1.1.1"]) # GET /bulk/{a,b,c}
Every successful JSON call returns an IpGeoResponse:
IpGeoResponse
├── success: bool
├── message: str | None # errors only
└── data: GeoData
├── ip: str
├── geo_location: GeoLocation
├── timezone: Timezone
├── flag: Flag
├── currency: Currency
├── connection: Connection
├── security: Security
├── user_agent: UserAgent # often present on /me
└── response_array: list # bulk only
Reading the full response (8.8.8.8)
Values below match the live IPWho API (Google DNS: United States, ASN 15169, timezone America/Chicago, dial code +1). Nested objects can be None for some IPs — always check.
resp = client.lookup("8.8.8.8")
assert resp.success
data = resp.data
print(data.ip) # "8.8.8.8"
geo = data.geo_location
print(geo.continent) # e.g. "North America"
print(geo.continent_code) # "NA"
print(geo.country) # "United States"
print(geo.country_code) # "US"
print(geo.capital)
print(geo.region)
print(geo.region_code)
print(geo.city)
print(geo.postal_code)
print(geo.dial_code) # "+1"
print(geo.is_in_eu) # False
print(geo.latitude, geo.longitude)
print(geo.accuracy_radius) # e.g. 1000
tz = data.timezone
print(tz.time_zone) # "America/Chicago"
print(tz.abbr, tz.offset, tz.is_dst, tz.utc, tz.current_time)
flag = data.flag
print(flag.flag_icon) # "🇺🇸"
print(flag.flag_unicode) # "U+1F1FA U+1F1F8"
cur = data.currency
print(cur.code, cur.symbol, cur.name)
print(cur.name_plural) # "US dollars"
print(cur.hex_unicode)
conn = data.connection
print(conn.asn_number) # 15169
print(conn.asn_org) # "Google LLC"
print(conn.isp, conn.org, conn.domain)
print(conn.connection_type) # "Corporate"
sec = data.security
print(sec.is_vpn, sec.is_tor, sec.is_threat) # threat: "low" | "medium" | "high"
if data.user_agent:
print(data.user_agent.browser.name, data.user_agent.os.name)
print(data.user_agent.device.type, data.user_agent.cpu.architecture)
me = client.me()
print(me.data.ip) # the caller's public IP
bulk = client.bulk(["8.8.8.8", "1.1.1.1"])
for item in bulk.data.response_array or []:
print(item.data.ip, item.data.geo_location.country)
Example JSON (mapped fields)
What lookup("8.8.8.8") looks like after the SDK maps the wire payload:
{
"success": true,
"data": {
"ip": "8.8.8.8",
"geo_location": {
"continent": "North America",
"continent_code": "NA",
"country": "United States",
"country_code": "US",
"capital": "Washington",
"region": "California",
"region_code": "CA",
"city": null,
"postal_code": null,
"dial_code": "+1",
"is_in_eu": false,
"latitude": 37.751,
"longitude": -97.822,
"accuracy_radius": 1000
},
"timezone": {
"time_zone": "America/Chicago",
"abbr": "CDT",
"offset": -18000,
"is_dst": true,
"utc": "UTC-05:00",
"current_time": "2026-08-07T12:00:00-05:00"
},
"flag": {
"flag_icon": "🇺🇸",
"flag_unicode": "U+1F1FA U+1F1F8"
},
"currency": {
"code": "USD",
"symbol": "$",
"name": "US Dollar",
"name_plural": "US dollars",
"hex_unicode": "0024"
},
"connection": {
"asn_number": 15169,
"asn_org": "Google LLC",
"isp": "Google LLC",
"org": "Google LLC",
"domain": "google.com",
"connection_type": "Corporate"
},
"security": {
"is_vpn": false,
"is_tor": false,
"is_threat": "low"
},
"user_agent": null
}
}
City/region on anycast DNS IPs may be empty; country, ASN, timezone, flag, and currency are populated. Exact coordinates vary.
Migrating from v1
| v1 | v2 |
|---|---|
get_ip(ip) / get_location(ip) |
lookup(ip) then resp.data.geo_location |
get_me() / get_location() |
me() |
get_timezone(ip) |
lookup(ip).data.timezone |
get_connection(ip) |
lookup(ip).data.connection |
get_security(ip) |
lookup(ip).data.security |
| (missing) | bulk(ips) |
Client class is IPWhoClient (was IPWho). Calls are synchronous.
API Reference
IPWhoClient(api_key, base_url=None, timeout=30.0)
- api_key: IPWho API key (sent as query
apiKey). Required. - base_url: default
https://api.ipwho.org. - timeout: seconds (default
30). - Raises:
ValueErrorif the key is empty.
lookup(ip, format="json", fields=None) -> IpGeoResponse
GET /ip/{ip}. format: json (typed), xml, or csv. fields: optional comma-separated objects, e.g. "geoLocation,timezone".
me(format="json", fields=None) -> IpGeoResponse
GET /me — same shape as lookup, for the caller's IP.
bulk(ips) -> IpGeoResponse
GET /bulk/{ip1,ip2,...}. ips must be non-empty. Per-IP rows: resp.data.response_array (list of IpGeoResponse).
Errors
InvalidIPError— HTTP 404RateLimitError— HTTP 429APIResponseError— other HTTP /success: falseIPWhoError— base class
Type Definitions
@dataclass
class IpGeoResponse:
success: bool
data: Optional[GeoData]
message: Optional[str]
@dataclass
class GeoData:
ip: str
geo_location: Optional[GeoLocation]
timezone: Optional[Timezone]
flag: Optional[Flag]
currency: Optional[Currency]
connection: Optional[Connection]
security: Optional[Security]
user_agent: Optional[UserAgent]
response_array: Optional[List[IpGeoResponse]] # bulk
@dataclass
class GeoLocation:
continent: Optional[str]
continent_code: Optional[str]
country: Optional[str]
country_code: Optional[str]
capital: Optional[str]
region: Optional[str]
region_code: Optional[str]
city: Optional[str]
postal_code: Optional[str]
dial_code: Optional[str]
is_in_eu: Optional[bool]
latitude: Optional[float]
longitude: Optional[float]
accuracy_radius: Optional[float]
@dataclass
class Timezone:
time_zone: Optional[str]
abbr: Optional[str]
offset: Optional[int]
is_dst: Optional[bool]
utc: Optional[str]
current_time: Optional[str]
@dataclass
class Flag:
flag_icon: Optional[str]
flag_unicode: Optional[str]
@dataclass
class Currency:
code: Optional[str]
symbol: Optional[str]
name: Optional[str]
name_plural: Optional[str]
hex_unicode: Optional[str]
@dataclass
class Connection:
asn_number: Optional[int]
asn_org: Optional[str]
isp: Optional[str]
org: Optional[str]
domain: Optional[str]
connection_type: Optional[str]
@dataclass
class Security:
is_vpn: Optional[bool]
is_tor: Optional[bool]
is_threat: Optional[str] # "low" | "medium" | "high"
@dataclass
class UserAgent:
browser: Optional[Browser] # name, version
engine: Optional[Engine]
os: Optional[OS]
device: Optional[Device] # type, vendor, model
cpu: Optional[CPU] # architecture
The live JSON mixes camelCase and snake_case (postal_Code, flag_Icon, isVpn). The SDK maps those onto the fields above.
Troubleshooting
- API key is required: create a key at ipwho.org.
- HTTP 403: blank User-Agent is rejected. This SDK sends
ipwho-python-sdk/2.0.0. - HTTP 401 / invalid key:
APIResponseError. - HTTP 429:
RateLimitError— back off and retry. - HTTP 404:
InvalidIPError. - None nested objects: not every IP has city, postal code, or user-agent.
Testing
IPWHO_API_KEY=your_key python3 test_ipwho.py
The live check is test_ipwho.py.
Changelog
v2.0.0
lookup/me/bulkmatching api.ipwho.org- Full
IpGeoResponse(geo, timezone, flag, currency, connection, security, user-agent) - Breaking change from v1
IPWho.get_location/get_ip
License
MIT License — see LICENSE.
Support
- Documentation: ipwho.org/docs
- Contact: ipwho.org/contact
- GitHub Issues: lavrox/SDK-IPWho-Python
- Website: ipwho.org
Lavrox — Independent API infrastructure. Lower latency, lower cost.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ipwho_ip_geolocation_api-2.0.0.tar.gz.
File metadata
- Download URL: ipwho_ip_geolocation_api-2.0.0.tar.gz
- Upload date:
- Size: 13.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c863e7387afefa4378058ce4c673261c9aaa4761ecfed5abb2665ff3288c61a5
|
|
| MD5 |
4e66973dd0001826b9b0454c7dbf6d3d
|
|
| BLAKE2b-256 |
b800ee537c086876be386db24a8368c0a5fa210ad5b65407a6cf1197423508cb
|
File details
Details for the file ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl.
File metadata
- Download URL: ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl
- Upload date:
- Size: 10.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2e73a966d807a155cce18af1e7f5418049a206528ac7379057888aab856ef65
|
|
| MD5 |
2386e8c21b0bf9491bc9f0b075484f7d
|
|
| BLAKE2b-256 |
bb084ee3e56b8d1028d825b15a348c6f1b413db00f1a41f7f4b833c068db68d3
|