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.

Metadata

Release files for ipwho-ip-geolocation-api 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ipwho-ip-geolocation-api 2.0.0
File Size Uploaded Provenance Actions
ipwho_ip_geolocation_api-2.0.0.tar.gz 13.8 kB Without provenance Details for ipwho_ip_geolocation_api-2.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ipwho-ip-geolocation-api 2.0.0
File Interpreter ABI Platform Actions
ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl Python 3 none any Details for ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl

Total release size: 23.9 kB

Release files / ipwho_ip_geolocation_api-2.0.0.tar.gz

Download URL ipwho_ip_geolocation_api-2.0.0.tar.gz
Size 13.8 kB
Tags Source
SHA-256 checksum
How to use checksums
c863e7387afefa4378058ce4c673261c9aaa4761ecfed5abb2665ff3288c61a5
BLAKE2b-256 checksum
How to use checksums
b800ee537c086876be386db24a8368c0a5fa210ad5b65407a6cf1197423508cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl

Download URL ipwho_ip_geolocation_api-2.0.0-py3-none-any.whl
Size 10.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c2e73a966d807a155cce18af1e7f5418049a206528ac7379057888aab856ef65
BLAKE2b-256 checksum
How to use checksums
bb084ee3e56b8d1028d825b15a348c6f1b413db00f1a41f7f4b833c068db68d3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Security for ipwho-ip-geolocation-api 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page