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

2026-08-16

IP-Ninja.com

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! 🥷