Introducing the IP Ninja Python Client: First-Class Access to the OSINT API
2026-08-16

Security researchers, threat hunters, and infrastructure analysts already know IP-Ninja for its fast OSINT API — reverse IP, subdomain enumeration, geolocation, WHOIS/RDAP, and ASN search, all delivered as structured JSON.
Today we are making it even easier to integrate that power directly into your Python workflows. We are proud to announce the official ipninja Python client, now available on PyPI and archived on GitHub.
Installation
The client is a lightweight package with no heavy transitive dependencies — just httpx, pydantic, and limits:
pip install ipninja
Or with Poetry:
poetry add ipninja
Requirements: Python 3.11+
Quick Start
Store your API key in an environment variable or a .env file — never hardcode it.
Synchronous:
import os
from ipninja import IPNinjaClient
with IPNinjaClient(api_key=os.environ["IPNINJA_API_KEY"]) as client:
result = client.reverse("1.2.3.4")
print(result.hostnames)
Asynchronous:
import asyncio, os
from ipninja import AsyncIPNinjaClient
async def main():
async with AsyncIPNinjaClient(api_key=os.environ["IPNINJA_API_KEY"]) as client:
result = await client.reverse("1.2.3.4")
print(result.hostnames)
asyncio.run(main())
That's it. Both clients expose the same methods; async methods are simply awaited.
Smart, Plan-Aware Rate Limiting
Every IP-Ninja subscription plan comes with different per-endpoint rate limits. The client doesn't guess — on the very first request it calls /v1/me to fetch your exact plan limits and configures a local moving-window rate limiter accordingly.
This means requests that would be rejected are never sent in the first place. No wasted network round-trips, no 429 surprises.
me = client.me()
print(f"Plan: {me.plan}")
for name, ep in me.endpoints.items():
if ep.rate_limit:
print(f" {name}: {ep.rate_limit.rate}/{ep.rate_limit.period}, "
f"result_limit={ep.result_limit}")
Controlling Over-Limit Behaviour
When a limit is reached — either client-side or via a server-side 429 — you control the behaviour with on_rate_limit:
| Value | Behaviour |
|---|---|
"wait" (default) |
Sleep until the window resets (or until Retry-After elapses), then retry transparently. |
"raise" |
Raise RateLimitExceededError immediately. The exception carries a retry_after attribute (seconds). |
Client-level default:
# Raise instead of waiting — useful for batch pipelines that manage retries themselves
client = IPNinjaClient(api_key=..., on_rate_limit="raise")
Per-request override:
with IPNinjaClient(api_key=...) as client: # default: "wait"
# This specific call raises instead of blocking
result = client.reverse("1.2.3.4", on_rate_limit="raise")
Handling RateLimitExceededError manually:
import time
from ipninja import IPNinjaClient, RateLimitExceededError
with IPNinjaClient(api_key=..., on_rate_limit="raise") as client:
try:
result = client.subdomains("example.com")
except RateLimitExceededError as e:
print(f"Rate limited. Retry in {e.retry_after:.0f}s.")
time.sleep(e.retry_after)
Concurrency-Safe Design
The sync client uses a threading.Lock to ensure the /v1/me prefetch happens exactly once. The async client uses a lazily-created asyncio.Lock so that concurrent coroutines never issue duplicate limit requests, even when they all start before the limits are loaded.
This makes asyncio.gather and batch pipelines safe out of the box:
import asyncio, os
from ipninja import AsyncIPNinjaClient
async def main():
ips = ["1.2.3.4", "5.6.7.8", "9.10.11.12"]
async with AsyncIPNinjaClient(api_key=os.environ["IPNINJA_API_KEY"]) as client:
results = await asyncio.gather(*[client.geoloc(ip) for ip in ips])
for ip, geo in zip(ips, results):
print(f"{ip} -> {geo.city}, {geo.country_code}")
asyncio.run(main())
Full API Reference
Both IPNinjaClient and AsyncIPNinjaClient expose identical methods. Below is a summary of every endpoint.
me() -> MeResponse
Returns your account info and the per-endpoint limits associated with your plan.
me = client.me()
print(me.plan) # e.g. "pro"
print(me.email)
reverse(ip) -> ReverseIPResult
Reverse IP lookup — hostnames and ASN currently associated with an IP.
result = client.reverse("1.2.3.4")
print(result.ip) # "1.2.3.4"
print(result.asn) # 12345 or None
print(result.hostnames) # ["example.com", ...]
history(ip) -> HostnamesResponse
Historical hostnames that have been tied to an IP address.
result = client.history("1.2.3.4")
print(result.hostnames)
resolve(domain) -> HostnamesResponse
DNS resolution — A and AAAA records for a domain.
result = client.resolve("example.com")
print(result.hostnames) # ["93.184.216.34"]
geoloc(ip) -> GeoLocationResponse
Geolocation and ASN intelligence for an IP.
geo = client.geoloc("1.2.3.4")
print(geo.city, geo.country_code) # "Paris", "FR"
print(geo.latitude, geo.longitude)
print(geo.asn, geo.asn_organization)
print(geo.network) # "1.2.3.0/24"
subdomains(domain) -> SubdomainsResponse
Known subdomains for a domain. The number of results returned depends on your plan's result_limit.
result = client.subdomains("example.com")
print(result.subdomains) # ["api.example.com", "mail.example.com", ...]
asn(asn) -> HostnamesResponse
All hostnames within an ASN. Accepts the ASN as an integer or string (with or without the AS prefix).
result = client.asn(15169)
# or: client.asn("AS15169")
print(result.hostnames)
ipwhois(ip) -> IPWhoisResponse
WHOIS data for an IP address.
whois = client.ipwhois("1.2.3.4")
print(whois.cidr) # "1.2.3.0/24"
print(whois.name)
print(whois.registration_date)
if whois.contacts and whois.contacts.abuse:
print(whois.contacts.abuse.email)
whois(domain) -> DomainWhoisResponse
WHOIS data for a domain (Beta).
whois = client.whois("example.com")
print(whois.domain)
print(whois.dates.created, whois.dates.expires)
print(whois.nameservers)
if whois.registrar:
print(whois.registrar.name)
Response Models
All methods return Pydantic v2 models. Every field that the API marks as nullable is typed ... | None, so you get proper static analysis and autocomplete support in your IDE.
| Model | Returned by |
|---|---|
MeResponse |
me() |
ReverseIPResult |
reverse() |
HostnamesResponse |
history(), resolve(), asn() |
SubdomainsResponse |
subdomains() |
GeoLocationResponse |
geoloc() |
IPWhoisResponse |
ipwhois() |
DomainWhoisResponse |
whois() |
All models are exported from the top-level ipninja package.
Exception Hierarchy
| Exception | Raised when |
|---|---|
IPNinjaError |
Base class for all library errors. |
AuthenticationError |
The API key is invalid or missing (HTTP 401). |
RateLimitExceededError |
Rate limit hit and on_rate_limit="raise". Has .retry_after (float, seconds). |
APIError |
Any other non-2xx response. Has .status_code. |
from ipninja import IPNinjaClient, AuthenticationError, APIError
try:
with IPNinjaClient(api_key=os.environ["IPNINJA_API_KEY"]) as client:
result = client.geoloc("1.2.3.4")
except AuthenticationError:
print("Check your API key!")
except APIError as e:
print(f"Unexpected error: {e.status_code}")
Development & Contributing
The library is open-source under the MIT license. Contributions are welcome!
git clone https://github.com/ipninja/ipninja.git
cd ipninja
poetry install
# run tests
poetry run pytest
# lint + format
poetry run black ipninja/ tests/
poetry run pylint ipninja/
The test suite uses pytest-httpx to mock API responses, so no network access is required.
Get Started
The ipninja package is available now on PyPI and GitHub.
Install it, point it at your API key, and start enriching IP data in seconds — whether you're writing a quick script or building a distributed OSINT pipeline.
pip install ipninja
For the full documentation and source code, visit the GitHub repository.
Happy hunting! 🥷