RDAP API Python SDK — Getting Started Guide
February 24, 2026
Need to look up domain registration data from Python? Whether you're building a security tool, monitoring brand domains, or enriching lead data, the RDAP protocol gives you structured JSON instead of the messy plain text you'd get from WHOIS.
You can hit RDAP servers directly, but you'll quickly run into inconsistent response formats across registries, rate limiting, and the hassle of chasing referrals between registry and registrar servers. The RDAP API handles all of that — you get a single, normalized JSON response every time.
This guide walks through the RDAP API Python SDK — from installation to real lookups in under five minutes.
Install the SDK
pip install rdapapi
Requires Python 3.9+. Dependencies (httpx, pydantic) install automatically.
Get your API key
Sign up at rdapapi.io/register to get an API key. You'll find it on your dashboard after verifying your email. Store it in an environment variable:
export RDAP_API_KEY="your-api-key-here"
Create the client
import os
from rdapapi import RdapApi
api = RdapApi(os.environ["RDAP_API_KEY"])
The client manages an HTTP connection pool under the hood (via httpx). For scripts, this is fine. For long-running applications, use it as a context manager so connections are cleaned up:
with RdapApi(os.environ["RDAP_API_KEY"]) as api:
domain = api.domain("google.com")
# connection pool is closed when the block exits
You can also configure timeout and base URL:
api = RdapApi(
os.environ["RDAP_API_KEY"],
timeout=10, # seconds (default: 30)
base_url="https://rdapapi.io/api/v1", # default
)
Your first domain lookup
domain = api.domain("google.com")
A basic lookup returns registration data from the registry — dates, nameservers, status, and registrar name:
domain.registrar.name # "MarkMonitor Inc."
domain.dates.expires # "2028-09-14T04:00:00Z"
domain.nameservers # ["ns1.google.com", "ns2.google.com", ...]
domain.dnssec # False
But notice what's missing — the entities object is empty:
domain.entities.registrant # None
That's because .com is a thin registry. Verisign stores the basics, but registrant details, abuse contacts, and richer registrar data live on the registrar's separate RDAP server. To get everything in one call, add follow=True:
domain = api.domain("google.com", follow=True)
domain.entities.registrant.organization # "Google LLC"
domain.entities.registrant.country_code # "US"
domain.registrar.abuse_email # "[email protected]"
The API chases the registrar referral automatically and merges both responses. Here's the full JSON you get back with follow=True:
{
"domain": "google.com",
"status": ["client delete prohibited", "client transfer prohibited", "..."],
"registrar": {
"name": "MarkMonitor Inc.",
"iana_id": "292",
"abuse_email": "[email protected]"
},
"dates": {
"registered": "1997-09-15T04:00:00Z",
"expires": "2028-09-14T04:00:00Z",
"updated": "2019-09-09T15:39:04Z"
},
"nameservers": ["ns1.google.com", "ns2.google.com", "ns3.google.com", "ns4.google.com"],
"dnssec": false,
"entities": {
"registrant": {
"organization": "Google LLC",
"country_code": "US"
}
}
}
Flat, predictable, every field in the same place. Try it live in the browser.
Beyond domains: IPs, ASNs, and more
The SDK supports all five RDAP object types with the same pattern — one method call, typed response:
# Find who operates an IP address and what network it belongs to
ip = api.ip("8.8.8.8")
ip.name # "GOGL"
ip.cidr # ["8.8.8.0/24"]
ip.country # "US"
# Look up an autonomous system by number
asn = api.asn(15169)
asn.name # "GOOGLE"
asn.handle # "AS15169"
# Resolve a nameserver to its IP addresses
ns = api.nameserver("ns1.google.com")
ns.ldh_name # "ns1.google.com"
ns.ip_addresses # IpAddresses(v4=["216.239.32.10"], v6=[...])
# Find all networks and ASNs tied to an organization
entity = api.entity("GOGL")
entity.name # "Google LLC"
entity.autnums # [EntityAutnum(handle="AS15169", ...)]
entity.networks # [EntityNetwork(cidr=["8.8.8.0/24"], ...)]
Working with responses
Missing fields return None
Not every registry provides every field. The SDK uses Pydantic models — optional fields return None instead of raising exceptions:
domain = api.domain("example.de")
# .de doesn't return registrant data via RDAP
if domain.entities.registrant:
print(domain.entities.registrant.organization)
else:
print("No registrant data available")
# Registrar fields can also be absent
domain.registrar.abuse_phone # None if not provided
List fields like nameservers and status default to empty lists, so you can always iterate without checking:
for ns in domain.nameservers: # safe even if empty
print(ns)
Dates are ISO 8601 strings
Date fields are returned as raw strings, not datetime objects. This keeps the SDK lightweight and avoids timezone ambiguity — parse them when you need to:
from datetime import datetime
expires_str = domain.dates.expires # "2028-09-14T04:00:00Z"
expires_dt = datetime.fromisoformat(expires_str)
days_left = (expires_dt - datetime.now(expires_dt.tzinfo)).days
print(f"{domain.domain} expires in {days_left} days")
Bulk domain lookups
Need to check multiple domains at once? The bulk_domains method looks up to 10 domains in a single request (requires Pro or Business plan):
result = api.bulk_domains(
["google.com", "github.com", "invalid..com"],
follow=True,
)
print(result.summary.total) # 3
print(result.summary.successful) # 2
print(result.summary.failed) # 1
for r in result.results:
if r.status == "success":
print(f"{r.data.domain}: expires {r.data.dates.expires}")
else:
print(f"{r.domain}: {r.error} — {r.message}")
Each domain in the request counts as one lookup toward your monthly quota. Invalid domains return inline errors instead of raising exceptions, so one bad domain doesn't break the entire batch.
Error handling
The SDK raises typed exceptions so you don't have to check status codes manually:
from rdapapi import NotFoundError, RateLimitError
try:
domain = api.domain("this-domain-does-not-exist-xyz.com")
except NotFoundError:
print("Domain not found in RDAP")
except RateLimitError as e:
print(f"Rate limited — retry after {e.retry_after}s")
Here's the full exception hierarchy:
| Exception | HTTP status | When it's raised |
|---|---|---|
RdapApiError |
any | Base class for all API errors |
ValidationError |
400 | Invalid input (e.g. malformed domain) |
AuthenticationError |
401 | Missing or invalid API key |
SubscriptionRequiredError |
403 | No active subscription |
NotFoundError |
404 | No RDAP data for this query |
RateLimitError |
429 | Rate limit or monthly quota exceeded |
UpstreamError |
502 | Upstream RDAP server failed |
All exceptions have status_code, error, and message attributes. RateLimitError adds retry_after (seconds until you can retry, or None).
Async support
The SDK ships with an async client for use with asyncio. The API is identical — just await the calls:
from rdapapi import AsyncRdapApi
async with AsyncRdapApi(os.environ["RDAP_API_KEY"]) as api:
domain = await api.domain("google.com", follow=True)
ip = await api.ip("8.8.8.8")
Same methods, same response types, same exceptions. Use AsyncRdapApi when you're already in an async context (FastAPI, aiohttp, etc.) to avoid blocking the event loop.
Next steps
- API documentation — full endpoint reference with interactive examples
- Python SDK on GitHub — source code and contributions welcome
- Pricing — plans start at $9/month with a 7-day free trial
- Start your free trial — get your API key and start building