Skip to main content

IPWho (ipwho.org) Python SDK

PyPI version Python version license

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).

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: ValueError if 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 404
  • RateLimitError — HTTP 429
  • APIResponseError — other HTTP / success: false
  • IPWhoError — 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 / bulk matching 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


IPWho — a Lavrox network API.

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

ipwho_ip_geolocation_api-2.0.0.tar.gz (13.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl (10.1 kB view details)

Uploaded Python 3

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

Hashes for ipwho_ip_geolocation_api-2.0.0.tar.gz
Algorithm Hash digest
SHA256 c863e7387afefa4378058ce4c673261c9aaa4761ecfed5abb2665ff3288c61a5
MD5 4e66973dd0001826b9b0454c7dbf6d3d
BLAKE2b-256 b800ee537c086876be386db24a8368c0a5fa210ad5b65407a6cf1197423508cb

See more details on using hashes here.

File details

Details for the file ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c2e73a966d807a155cce18af1e7f5418049a206528ac7379057888aab856ef65
MD5 2386e8c21b0bf9491bc9f0b075484f7d
BLAKE2b-256 bb084ee3e56b8d1028d825b15a348c6f1b413db00f1a41f7f4b833c068db68d3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page