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


Ready to try RDAP lookups?