Skip to main content

Ipregistry FastAPI

License Tests PyPI

Official Ipregistry integration for FastAPI: dependency-driven IP geolocation and threat detection for every request, built on the asynchronous Ipregistry Python client.

from fastapi import FastAPI
from ipregistry_fastapi import IpInfoDep, ipregistry_lifespan

app = FastAPI(lifespan=ipregistry_lifespan)

@app.get("/")
async def index(info: IpInfoDep):
    return {"country": info.location.country.name if info else None}

Features

  • Async-first: non-blocking lookups through the official asynchronous Ipregistry client, with connection reuse managed by the application lifespan.
  • Idiomatic dependencies: IpInfoDep, IpregistryDep and ClientIpDep annotated types plug straight into path operations — HTTP and WebSocket alike; one cached lookup per request, shared by every consumer. All dependencies are coroutines: no threadpool dispatch on the request path.
  • Guards: block countries (HTTP 451) and threats (HTTP 403) per route, per router, or app-wide.
  • Middleware: optional pure-ASGI middleware for global enrichment and blocking, with static asset and bot skipping to save credits.
  • Safe by default: fails open, never sends private or reserved addresses to the API, validates proxy headers, and never logs full client IPs.
  • Configuration via environment: IPREGISTRY_* variables parsed with pydantic-settings.
  • Typed responses: lookups return the client's Pydantic v2 models, ready to embed in your own response models.
  • First-class testing: IpregistryFake swaps in canned responses with call assertions — no network, no API key.

Getting started

Requirements

  • Python 3.10+
  • FastAPI 0.115+
  • An Ipregistry API key — the free tier includes 100,000 lookups

Installation

pip install ipregistry-fastapi

or with uv:

uv add ipregistry-fastapi

Quick start

Set your API key:

export IPREGISTRY_API_KEY=YOUR_API_KEY

Wire the lifespan and use the dependency:

from fastapi import FastAPI
from ipregistry_fastapi import IpInfoDep, ipregistry_lifespan

app = FastAPI(lifespan=ipregistry_lifespan)

@app.get("/whoami")
async def whoami(info: IpInfoDep):
    if info is None:
        return {"detail": "No IP intelligence available"}
    return {
        "ip": info.ip,
        "country": info.location.country.name,
        "city": info.location.city,
        "threat": info.security.is_threat,
    }

IpInfoDep resolves to an IpInfo model — or None when the client IP is private (e.g. on localhost) or the lookup failed. One lookup runs per request no matter how many dependencies, guards or middleware consume it, and responses are cached in memory across requests.

To configure programmatically instead of via the environment:

from ipregistry_fastapi import IpregistrySettings, add_ipregistry, ipregistry_lifespan

app = FastAPI(lifespan=ipregistry_lifespan)
add_ipregistry(app, IpregistrySettings(api_key="...", base_url="eu", fields="location,security"))

Blocking countries

block_countries returns a dependency enforcing an ISO 3166-1 alpha-2 country list. Blocked visitors receive 451 Unavailable For Legal Reasons by default. Invalid codes fail at declaration time, not at request time.

from fastapi import APIRouter, Depends
from ipregistry_fastapi import block_countries

router = APIRouter(dependencies=[Depends(block_countries("KP", "SY"))])

# Allowlist mode: only serve the EU single market
checkout = APIRouter(dependencies=[Depends(block_countries(EU_COUNTRIES, mode="allow"))])

Requests whose country cannot be determined are allowed unless you pass unknown="block".

Blocking threats

block_threats blocks IPs flagged as threats, attackers or abusers, and responds with 403. Anonymization signals are opt-in — a VPN user is not necessarily hostile:

from fastapi import Depends, FastAPI
from ipregistry_fastapi import block_threats

app = FastAPI(dependencies=[Depends(block_threats())])

# Stricter variant for sensitive routes:
admin = APIRouter(dependencies=[Depends(block_threats(proxy=True, tor=True, vpn=True, relay=True))])

Requests without security data are allowed (fail open), so an Ipregistry outage never locks users out of your API.

Middleware

Prefer dependencies for per-route rules. Reach for the middleware to enforce app-wide policies or eagerly enrich every request — including ones that never match a route:

from ipregistry_fastapi import IpregistryMiddleware, block_countries, block_threats

app.add_middleware(
    IpregistryMiddleware,
    actions=[block_countries("KP"), block_threats(tor=True)],
    skip_bots=True,          # skip lookups for crawler user agents
    skip_static_assets=True, # default: skip .css/.js/images/fonts
    skip=lambda request: request.url.path.startswith("/health"),
)

The middleware stores the lookup on request.state.ipregistry, so IpInfoDep and route guards reuse it for free.

Fail open or closed

By default a failed lookup lets the request proceed with info = None (fail open). To refuse requests instead when IP intelligence is unavailable:

  • set IPREGISTRY_FAIL_OPEN=false — IpInfoDep and blocking dependencies respond with 503;
  • or pass fail_closed=True (or a custom status code) to IpregistryMiddleware.

GDPR & EU detection

from ipregistry_fastapi import IpInfoDep, is_eu

@app.get("/consent")
async def consent(info: IpInfoDep):
    return {"show_banner": is_eu(info, assume_eu=True)}

is_eu reads location.in_eu; assume_eu=True errs on the side of showing a consent banner when the location is unknown. For EU data residency, set IPREGISTRY_BASE_URL=eu to route all API requests to https://eu.api.ipregistry.co.

Related helpers: is_threat(info, vpn=True, ...) and is_bot(request) (heuristic, no API call).

Configuration

All settings load from the environment via pydantic-settings, or can be passed programmatically with IpregistrySettings:

Environment variable Default Description
IPREGISTRY_API_KEY — Required. Your Ipregistry API key.
IPREGISTRY_BASE_URL default API endpoint; eu selects the EU-only endpoint; full URLs verbatim.
IPREGISTRY_FIELDS all Default field selection, e.g. location,security.
IPREGISTRY_HOSTNAME false Resolve the hostname of looked up IPs.
IPREGISTRY_TIMEOUT 5.0 Request timeout in seconds.
IPREGISTRY_RETRIES 1 Retry attempts after a failed request.
IPREGISTRY_RETRY_INTERVAL 0.5 Base delay between retries (exponential backoff).
IPREGISTRY_RETRY_ON_SERVER_ERROR true Retry on 5xx responses.
IPREGISTRY_RETRY_ON_TOO_MANY_REQUESTS false Retry on 429 responses (honors Retry-After).
IPREGISTRY_CACHE_ENABLED true Cache lookups in memory.
IPREGISTRY_CACHE_MAX_SIZE 2048 Maximum cached lookups.
IPREGISTRY_CACHE_TTL 600 Cache time-to-live in seconds.
IPREGISTRY_DEVELOPMENT_IP — Fallback IP when the client IP is private (e.g. localhost).
IPREGISTRY_FAIL_OPEN true Proceed without data when a lookup fails.
IPREGISTRY_IP_SOURCE client How to resolve the client IP (see below).

Selecting only the fields you need (e.g. IPREGISTRY_FIELDS=location,security) reduces latency and response size.

IP extraction behind proxies

By default (client) the library uses the ASGI client address — the right choice when uvicorn runs with --proxy-headers --forwarded-allow-ips or behind ProxyHeadersMiddleware, which already resolve the real client IP.

If your server does not rewrite the client address, pick the header set by a proxy you trust:

IPREGISTRY_IP_SOURCE Headers consulted (in order)
client (default) none — ASGI client address
auto X-Real-IP, X-Forwarded-For
cloudflare CF-Connecting-IP
nginx X-Real-IP, X-Forwarded-For
forwarded-for X-Forwarded-For (first entry)
header:<name> the named header

A callable ip_extractor can also be passed to Ipregistry/add_ipregistry for full control. Values are sanitized (ports, IPv6 brackets and zone IDs stripped) and validated; only configure headers your proxy overwrites — client-supplied headers are trivially spoofable. Private, loopback and carrier-grade NAT addresses are never sent to the API; set IPREGISTRY_DEVELOPMENT_IP to test with a fixed address on localhost.

Caching and credits

Lookups are cached in memory (2048 entries, 10 minutes by default), so repeated requests from the same address consume a single credit. On top of that:

  • one lookup per request, however many consumers;
  • static asset and bot skipping in the middleware;
  • private/reserved IPs short-circuit before reaching the API.

Multi-worker deployments (uvicorn --workers, gunicorn) hold one cache per process.

Testing your app

IpregistryFake is a drop-in service with canned responses and no network access:

from fastapi.testclient import TestClient
from ipregistry_fastapi import add_ipregistry
from ipregistry_fastapi.testing import IpregistryFake

fake = IpregistryFake({
    "66.165.2.7": {"location": {"country": {"code": "US"}}},
    "*": {"security": {"is_threat": False}},   # any other address
})
add_ipregistry(app, service=fake)

client = TestClient(app)
response = client.get("/whoami", headers={"x-real-ip": "66.165.2.7"})

fake.assert_looked_up("66.165.2.7")

Stubs are API-shaped dicts, IpInfo instances, or exceptions to raise. Unlike the real service, the fake accepts private addresses so requests made through TestClient resolve out of the box. Assertions: assert_looked_up, assert_not_looked_up, assert_looked_up_times, assert_nothing_looked_up, assert_origin_looked_up, assert_user_agents_parsed.

FastAPI's standard dependency_overrides mechanism works too, and propagates to IpInfoDep and the blocking guards:

from ipregistry_fastapi import get_ipregistry

app.dependency_overrides[get_ipregistry] = lambda: fake

Errors

Request-aware helpers (IpInfoDep, guards, middleware) never raise on lookup failure — they fail open and record the exception on request.state.ipregistry_error. Explicit lookups raise the client's exceptions:

from ipregistry_fastapi import ApiError, ClientError, ErrorCode, IpregistryDep

@app.get("/lookup/{ip}")
async def lookup(ip: str, ipregistry: IpregistryDep):
    try:
        return await ipregistry.lookup(ip)
    except ApiError as error:
        if error.error_code == ErrorCode.INVALID_IP_ADDRESS:
            raise HTTPException(status_code=400, detail=error.message)
        raise HTTPException(status_code=502, detail="Lookup failed")
    except ClientError:
        raise HTTPException(status_code=504, detail="Ipregistry unreachable")

Beyond IP lookups

The full asynchronous client remains available for batch lookups, ASN data, user-agent parsing and credits introspection:

@app.post("/enrich")
async def enrich(ips: list[str], ipregistry: IpregistryDep):
    results = await ipregistry.lookup_batch(ips)          # order-preserving
    origin = await ipregistry.lookup_origin()             # the server's own IP
    asn = await ipregistry.client.lookup_asn(400923)      # raw client escape hatch

Migrating from ipregistry-python

Existing ipregistry client code keeps working — this package layers FastAPI wiring on top:

  • replace manual client creation with ipregistry_lifespan / add_ipregistry;
  • replace per-endpoint lookups of the request IP with IpInfoDep;
  • move country/threat checks into block_countries / block_threats dependencies;
  • configuration moves to IPREGISTRY_* environment variables.

Other resources

License

Apache-2.0 © Ipregistry

Metadata

Release files for ipregistry-fastapi 1.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 ipregistry-fastapi 1.0.0
File Size Uploaded
ipregistry_fastapi-1.0.0.tar.gz 22.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ipregistry-fastapi 1.0.0
File Interpreter ABI Platform
ipregistry_fastapi-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 53.4 kB

Release files / ipregistry_fastapi-1.0.0.tar.gz

Download URL ipregistry_fastapi-1.0.0.tar.gz
Size 22.1 kB
Tags Source
SHA-256 checksum
How to use checksums
23bfca24e2a096412a0a76925a58743d5863176c3e0bd1fc306f8e9b3a566dd9
BLAKE2b-256 checksum
How to use checksums
bc9c63f8891adbc937731fae2dbcbe7ad0305cb3e0771daeb600b9e95ef12208
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.

Transparency log

Release files / ipregistry_fastapi-1.0.0-py3-none-any.whl

Download URL ipregistry_fastapi-1.0.0-py3-none-any.whl
Size 31.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b54c9e4ccc47dd9e869650f54c364ae492785014e315a63408490c4c9388ccbc
BLAKE2b-256 checksum
How to use checksums
e09bcd2fb8d8cfaa812d7de825f98e705a238aae9df5e9b64ce52d92dba16252
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

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