Quick Answer
RDAP (Registration Data Access Protocol) can be queried programmatically using standard HTTP GET requests to registry-operated RDAP servers. Use https://rdap.org/domain/{domain} for simple lookups or query registries directly for production use. RDAP returns structured JSON data including domain status, nameservers, registrant information, and registration dates. Implement rate limiting (1-2 requests per second), proper error handling, and response parsing for robust integration. RDAP is the modern replacement for WHOIS with better structure, internationalization, and machine-readability.
Table of Contents
- Introduction to RDAP API
- RDAP Basics and Architecture
- Finding the Right RDAP Server
- Making Your First RDAP Query
- JavaScript/Node.js Examples
- Python Examples
- Understanding RDAP Responses
- Parsing RDAP Data
- Error Handling
- Rate Limiting and Best Practices
- Advanced RDAP Queries
- Building an RDAP Client Library
- RDAP vs WHOIS for Developers
- Common Use Cases
- Frequently Asked Questions
- Key Takeaways
- Next Steps
- Research Sources
Introduction to RDAP API
RDAP (Registration Data Access Protocol) provides a standardized, modern API for querying domain registration data.
What is RDAP?
RDAP Characteristics:
- RESTful HTTP-based protocol
- Returns structured JSON data
- Standardized response format
- Internationalization support
- Authentication support (for restricted data)
- Rate limiting built-in
- Replace for WHOIS protocol
Why Use RDAP Programmatically?
Advantages over WHOIS:
-
Structured Data
WHOIS: Unstructured text, varies by registry RDAP: Standardized JSON, consistent format -
Machine-Readable
WHOIS: Requires custom parsing per registry RDAP: Native JSON parsing -
Better Internationalization
WHOIS: ASCII only, poor i18n support RDAP: Full Unicode support, multiple languages -
Standard Error Handling
WHOIS: Inconsistent error messages RDAP: Standard HTTP status codes + detailed errors -
Built-in Rate Limiting
WHOIS: Varies by server, often unclear RDAP: Standard HTTP 429 with retry-after headers
What You Can Query with RDAP
Available query types:
- Domain lookups (most common)
- Entity lookups (registrant, registrar)
- Nameserver lookups
- IP address lookups
- Autonomous System Number (ASN) lookups
Information returned:
- Domain status (EPP codes)
- Nameservers
- Registration dates (created, updated, expires)
- Registrar information
- Registrant information (if available)
- DNSSEC information
- Contact information (subject to privacy policies)
RDAP Use Cases for Developers
- Domain availability checking
- Domain portfolio management
- Security research and threat intelligence
- Compliance and monitoring tools
- Registrar/registry integrations
- Domain expiration monitoring
- WHOIS replacement in existing tools
RDAP Basics and Architecture
RDAP Architecture
Client Application
↓ (HTTPS GET Request)
RDAP Bootstrap Service (rdap.org)
↓ (Redirects to appropriate registry)
Registry RDAP Server
↓ (Returns JSON response)
Client Application
↓ (Parses JSON)
Extracted Data
RDAP Query Types
Domain Query:
GET https://rdap.org/domain/example.com
Entity Query (Registrar):
GET https://rdap.org/entity/292
Nameserver Query:
GET https://rdap.org/nameserver/ns1.example.com
IP Address Query:
GET https://rdap.org/ip/192.0.2.1
ASN Query:
GET https://rdap.org/autnum/64496
RDAP URL Structure
Generic format:
https://{rdap-server}/{object-type}/{query-value}
Components:
{rdap-server}: Registry RDAP server URL{object-type}: domain, entity, nameserver, ip, autnum{query-value}: The identifier you're looking up
Examples:
https://rdap.org/domain/google.com
https://rdap.verisign.com/com/v1/domain/example.com
https://rdap.org/ip/8.8.8.8
RDAP Response Format
All RDAP responses are JSON with standard structure:
{
"objectClassName": "domain",
"handle": "EXAMPLE-COM",
"ldhName": "example.com",
"status": ["client transfer prohibited"],
"entities": [...],
"nameservers": [...],
"events": [...],
"links": [...]
}
Standard fields:
objectClassName: Type of object (domain, entity, etc.)handle: Registry identifierldhName: ASCII domain namestatus: EPP status codesentities: Related entities (registrar, registrant)nameservers: Authoritative nameserversevents: Timestamps (registration, expiration, etc.)links: Related resources
Finding the Right RDAP Server
Different TLDs have different RDAP servers. You need to query the correct server for each TLD.
Option 1: Use RDAP.org Bootstrap Service
Simplest approach:
https://rdap.org/domain/{domain}
This service automatically:
- Detects the TLD from your query
- Looks up the correct RDAP server
- Redirects your query to the appropriate registry
- Returns the registry's response
Advantages:
- No TLD-specific logic needed
- Always up to date
- Handles redirects automatically
Disadvantages:
- Extra network hop (redirect)
- Dependency on rdap.org
- Slightly slower than direct queries
Option 2: Query Registries Directly
For production use, query registries directly:
.com/.net domains:
https://rdap.verisign.com/com/v1/domain/example.com
https://rdap.verisign.com/net/v1/domain/example.net
.org domains:
https://rdap.publicinterestregistry.org/rdap/domain/example.org
.io domains:
https://rdap.nic.io/domain/example.io
.ai domains:
https://rdap.nic.ai/domain/example.ai
Option 3: Use IANA Bootstrap Files
IANA publishes authoritative RDAP server lists:
Download and parse:
https://data.iana.org/rdap/dns.json
This JSON file maps TLDs to RDAP servers:
{
"services": [
[
["com", "net"],
["https://rdap.verisign.com/com/v1/", "https://rdap.verisign.com/net/v1/"]
],
[
["org"],
["https://rdap.publicinterestregistry.org/rdap/"]
]
]
}
Implementation approach:
- Download dns.json periodically (cache for 24h)
- Parse and build TLD → RDAP server mapping
- Look up server for queried domain's TLD
- Make direct query to that server
RDAP Server Directory
Common RDAP servers for popular TLDs:
| TLD | RDAP Server |
|---|---|
| .com | https://rdap.verisign.com/com/v1/ |
| .net | https://rdap.verisign.com/net/v1/ |
| .org | https://rdap.publicinterestregistry.org/rdap/ |
| .io | https://rdap.nic.io/ |
| .ai | https://rdap.nic.ai/ |
| .co | https://rdap.nic.co/ |
| .me | https://rdap.nic.me/ |
| .dev | https://rdap.nic.google/ |
| .app | https://rdap.nic.google/ |
| .uk | https://rdap.nominet.uk/ |
| .de | https://rdap.denic.de/ |
| .fr | https://rdap.nic.fr/ |
| .ca | https://rdap.ca.fury.ca/ |
Making Your First RDAP Query
Simple cURL Example
# Query using rdap.org bootstrap
curl -s https://rdap.org/domain/example.com | jq
# Query Verisign directly for .com
curl -s https://rdap.verisign.com/com/v1/domain/example.com | jq
# Save response to file
curl -s https://rdap.org/domain/example.com > response.json
# View specific fields
curl -s https://rdap.org/domain/example.com | jq '.ldhName, .status'
HTTP Headers
Request headers:
GET /domain/example.com HTTP/1.1
Host: rdap.org
Accept: application/rdap+json
User-Agent: MyApp/1.0 ([email protected])
Response headers:
HTTP/1.1 200 OK
Content-Type: application/rdap+json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1638360000
Testing RDAP Queries
Test domains for development:
example.com - Standard test domain
example.org - Alternative test domain
google.com - Real domain with full data
github.com - Another real example
Check response status:
# Check HTTP status code
curl -s -o /dev/null -w "%{http_code}" https://rdap.org/domain/example.com
# 200 = Success
# 404 = Domain not found
# 429 = Rate limited
# 500 = Server error
JavaScript/Node.js Examples
Basic RDAP Query (Node.js)
// Using native fetch (Node.js 18+)
async function queryRDAP(domain) {
const url = `https://rdap.org/domain/${domain}`;
try {
const response = await fetch(url, {
headers: {
'Accept': 'application/rdap+json',
'User-Agent': 'MyApp/1.0 ([email protected])'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('RDAP query failed:', error.message);
throw error;
}
}
// Usage
queryRDAP('example.com')
.then(data => {
console.log('Domain:', data.ldhName);
console.log('Status:', data.status);
console.log('Nameservers:', data.nameservers?.map(ns => ns.ldhName));
})
.catch(error => {
console.error('Error:', error);
});
Advanced RDAP Client (Node.js)
class RDAPClient {
constructor(options = {}) {
this.userAgent = options.userAgent || 'RDAPClient/1.0';
this.timeout = options.timeout || 10000; // 10 seconds
this.cache = new Map();
this.cacheTTL = options.cacheTTL || 3600000; // 1 hour
}
/**
* Query RDAP for a domain
*/
async queryDomain(domain) {
// Check cache first
const cached = this.getFromCache(domain);
if (cached) {
return cached;
}
const url = `https://rdap.org/domain/${domain}`;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(url, {
headers: {
'Accept': 'application/rdap+json',
'User-Agent': this.userAgent
},
signal: controller.signal
});
clearTimeout(timeoutId);
// Handle rate limiting
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
throw new Error(`Rate limited. Retry after ${retryAfter} seconds`);
}
// Handle not found
if (response.status === 404) {
return { error: 'Domain not found', status: 404 };
}
// Handle other errors
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Cache the result
this.setCache(domain, data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
/**
* Extract common fields from RDAP response
*/
parseDomain(rdapData) {
return {
domain: rdapData.ldhName,
status: rdapData.status || [],
nameservers: this.extractNameservers(rdapData),
events: this.extractEvents(rdapData),
registrar: this.extractRegistrar(rdapData),
dnssec: this.extractDNSSEC(rdapData)
};
}
/**
* Extract nameservers from RDAP response
*/
extractNameservers(rdapData) {
if (!rdapData.nameservers) return [];
return rdapData.nameservers.map(ns => ({
name: ns.ldhName,
ipAddresses: this.extractNSIPAddresses(ns)
}));
}
/**
* Extract IP addresses from nameserver
*/
extractNSIPAddresses(nameserver) {
const ips = {
ipv4: [],
ipv6: []
};
if (nameserver.ipAddresses) {
if (nameserver.ipAddresses.v4) {
ips.ipv4 = nameserver.ipAddresses.v4;
}
if (nameserver.ipAddresses.v6) {
ips.ipv6 = nameserver.ipAddresses.v6;
}
}
return ips;
}
/**
* Extract events (dates) from RDAP response
*/
extractEvents(rdapData) {
if (!rdapData.events) return {};
const events = {};
rdapData.events.forEach(event => {
switch (event.eventAction) {
case 'registration':
events.created = event.eventDate;
break;
case 'expiration':
events.expires = event.eventDate;
break;
case 'last changed':
events.updated = event.eventDate;
break;
}
});
return events;
}
/**
* Extract registrar information
*/
extractRegistrar(rdapData) {
if (!rdapData.entities) return null;
const registrar = rdapData.entities.find(entity =>
entity.roles && entity.roles.includes('registrar')
);
if (!registrar) return null;
return {
name: registrar.vcardArray?.[1]?.find(v => v[0] === 'fn')?.[3],
ianaid: registrar.publicIds?.find(id => id.type === 'IANA Registrar ID')?.identifier
};
}
/**
* Extract DNSSEC information
*/
extractDNSSEC(rdapData) {
return {
signed: rdapData.secureDNS?.delegationSigned || false,
dsData: rdapData.secureDNS?.dsData || []
};
}
/**
* Cache management
*/
getFromCache(key) {
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > this.cacheTTL) {
this.cache.delete(key);
return null;
}
return cached.data;
}
setCache(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
clearCache() {
this.cache.clear();
}
}
// Usage example
const client = new RDAPClient({
userAgent: 'MyDomainTool/1.0 ([email protected])',
cacheTTL: 3600000 // 1 hour
});
async function lookupDomain(domain) {
try {
const rdapData = await client.queryDomain(domain);
const parsed = client.parseDomain(rdapData);
console.log('Domain Information:');
console.log('------------------');
console.log('Domain:', parsed.domain);
console.log('Status:', parsed.status.join(', '));
console.log('Created:', parsed.events.created);
console.log('Expires:', parsed.events.expires);
console.log('Registrar:', parsed.registrar?.name);
console.log('DNSSEC:', parsed.dnssec.signed ? 'Signed' : 'Not signed');
console.log('\nNameservers:');
parsed.nameservers.forEach((ns, i) => {
console.log(` ${i + 1}. ${ns.name}`);
});
} catch (error) {
console.error('Lookup failed:', error.message);
}
}
lookupDomain('example.com');
Browser JavaScript Example
<!DOCTYPE html>
<html>
<head>
<title>RDAP Lookup Tool</title>
</head>
<body>
<h1>RDAP Domain Lookup</h1>
<input type="text" id="domain" placeholder="example.com">
<button onclick="lookupDomain()">Lookup</button>
<pre id="result"></pre>
<script>
async function lookupDomain() {
const domain = document.getElementById('domain').value.trim();
const resultElement = document.getElementById('result');
if (!domain) {
resultElement.textContent = 'Please enter a domain';
return;
}
resultElement.textContent = 'Querying...';
try {
const response = await fetch(`https://rdap.org/domain/${domain}`, {
headers: {
'Accept': 'application/rdap+json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Format and display result
const formatted = {
domain: data.ldhName,
status: data.status,
nameservers: data.nameservers?.map(ns => ns.ldhName),
events: data.events?.reduce((acc, event) => {
acc[event.eventAction] = event.eventDate;
return acc;
}, {})
};
resultElement.textContent = JSON.stringify(formatted, null, 2);
} catch (error) {
resultElement.textContent = `Error: ${error.message}`;
}
}
</script>
</body>
</html>
Python Examples
Basic RDAP Query (Python)
import requests
import json
from datetime import datetime
def query_rdap(domain):
"""
Query RDAP for domain information
"""
url = f"https://rdap.org/domain/{domain}"
headers = {
'Accept': 'application/rdap+json',
'User-Agent': 'MyApp/1.0 ([email protected])'
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # Raise exception for bad status codes
return response.json()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
return {'error': 'Domain not found'}
elif e.response.status_code == 429:
retry_after = e.response.headers.get('Retry-After', 'unknown')
return {'error': f'Rate limited. Retry after {retry_after} seconds'}
else:
raise
except requests.exceptions.Timeout:
return {'error': 'Request timeout'}
except requests.exceptions.RequestException as e:
return {'error': str(e)}
# Usage
if __name__ == '__main__':
domain = 'example.com'
data = query_rdap(domain)
if 'error' in data:
print(f"Error: {data['error']}")
else:
print(f"Domain: {data.get('ldhName')}")
print(f"Status: {', '.join(data.get('status', []))}")
print(f"Nameservers:")
for ns in data.get('nameservers', []):
print(f" - {ns.get('ldhName')}")
Advanced RDAP Client (Python)
import requests
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import time
class RDAPClient:
"""
Advanced RDAP client with caching, rate limiting, and parsing
"""
def __init__(self, user_agent='RDAPClient/1.0', cache_ttl=3600):
self.user_agent = user_agent
self.cache = {}
self.cache_ttl = cache_ttl # seconds
self.last_request_time = 0
self.min_request_interval = 1.0 # Rate limit: 1 request per second
def query_domain(self, domain: str) -> Dict:
"""
Query RDAP for a domain
"""
# Check cache
cached = self._get_from_cache(domain)
if cached:
return cached
# Rate limiting
self._rate_limit()
url = f"https://rdap.org/domain/{domain}"
headers = {
'Accept': 'application/rdap+json',
'User-Agent': self.user_agent
}
try:
response = requests.get(url, headers=headers, timeout=10)
# Handle rate limiting
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
raise Exception(f'Rate limited. Retry after {retry_after} seconds')
# Handle not found
if response.status_code == 404:
return {'error': 'Domain not found', 'status': 404}
response.raise_for_status()
data = response.json()
# Cache the result
self._set_cache(domain, data)
return data
except requests.exceptions.Timeout:
return {'error': 'Request timeout'}
except requests.exceptions.RequestException as e:
return {'error': str(e)}
def parse_domain(self, rdap_data: Dict) -> Dict:
"""
Extract common fields from RDAP response
"""
return {
'domain': rdap_data.get('ldhName'),
'status': rdap_data.get('status', []),
'nameservers': self._extract_nameservers(rdap_data),
'events': self._extract_events(rdap_data),
'registrar': self._extract_registrar(rdap_data),
'dnssec': self._extract_dnssec(rdap_data)
}
def _extract_nameservers(self, rdap_data: Dict) -> List[Dict]:
"""
Extract nameservers from RDAP response
"""
nameservers = []
for ns in rdap_data.get('nameservers', []):
ns_data = {
'name': ns.get('ldhName'),
'ipAddresses': {
'ipv4': [],
'ipv6': []
}
}
if 'ipAddresses' in ns:
ns_data['ipAddresses']['ipv4'] = ns['ipAddresses'].get('v4', [])
ns_data['ipAddresses']['ipv6'] = ns['ipAddresses'].get('v6', [])
nameservers.append(ns_data)
return nameservers
def _extract_events(self, rdap_data: Dict) -> Dict:
"""
Extract events (dates) from RDAP response
"""
events = {}
for event in rdap_data.get('events', []):
action = event.get('eventAction')
date = event.get('eventDate')
if action == 'registration':
events['created'] = date
elif action == 'expiration':
events['expires'] = date
elif action == 'last changed':
events['updated'] = date
return events
def _extract_registrar(self, rdap_data: Dict) -> Optional[Dict]:
"""
Extract registrar information
"""
entities = rdap_data.get('entities', [])
for entity in entities:
if 'registrar' in entity.get('roles', []):
# Extract name from vCard
name = None
if 'vcardArray' in entity:
for vcard_item in entity['vcardArray'][1]:
if vcard_item[0] == 'fn':
name = vcard_item[3]
break
# Extract IANA ID
iana_id = None
for public_id in entity.get('publicIds', []):
if public_id.get('type') == 'IANA Registrar ID':
iana_id = public_id.get('identifier')
break
return {
'name': name,
'ianaId': iana_id
}
return None
def _extract_dnssec(self, rdap_data: Dict) -> Dict:
"""
Extract DNSSEC information
"""
secure_dns = rdap_data.get('secureDNS', {})
return {
'signed': secure_dns.get('delegationSigned', False),
'dsData': secure_dns.get('dsData', [])
}
def _rate_limit(self):
"""
Implement rate limiting
"""
current_time = time.time()
time_since_last = current_time - self.last_request_time
if time_since_last < self.min_request_interval:
sleep_time = self.min_request_interval - time_since_last
time.sleep(sleep_time)
self.last_request_time = time.time()
def _get_from_cache(self, key: str) -> Optional[Dict]:
"""
Get item from cache if not expired
"""
if key not in self.cache:
return None
cached = self.cache[key]
# Check if expired
if datetime.now() - cached['timestamp'] > timedelta(seconds=self.cache_ttl):
del self.cache[key]
return None
return cached['data']
def _set_cache(self, key: str, data: Dict):
"""
Set item in cache
"""
self.cache[key] = {
'data': data,
'timestamp': datetime.now()
}
def clear_cache(self):
"""
Clear all cached data
"""
self.cache.clear()
# Usage example
def lookup_domain(domain: str):
"""
Lookup and display domain information
"""
client = RDAPClient(
user_agent='MyDomainTool/1.0 ([email protected])',
cache_ttl=3600
)
print(f"Querying RDAP for {domain}...")
rdap_data = client.query_domain(domain)
if 'error' in rdap_data:
print(f"Error: {rdap_data['error']}")
return
parsed = client.parse_domain(rdap_data)
print("\nDomain Information:")
print("-" * 50)
print(f"Domain: {parsed['domain']}")
print(f"Status: {', '.join(parsed['status'])}")
events = parsed['events']
if 'created' in events:
print(f"Created: {events['created']}")
if 'expires' in events:
print(f"Expires: {events['expires']}")
if 'updated' in events:
print(f"Updated: {events['updated']}")
if parsed['registrar']:
print(f"Registrar: {parsed['registrar']['name']}")
print(f"Registrar IANA ID: {parsed['registrar']['ianaId']}")
print(f"DNSSEC: {'Signed' if parsed['dnssec']['signed'] else 'Not signed'}")
print("\nNameservers:")
for i, ns in enumerate(parsed['nameservers'], 1):
print(f" {i}. {ns['name']}")
if ns['ipAddresses']['ipv4']:
print(f" IPv4: {', '.join(ns['ipAddresses']['ipv4'])}")
if ns['ipAddresses']['ipv6']:
print(f" IPv6: {', '.join(ns['ipAddresses']['ipv6'])}")
if __name__ == '__main__':
lookup_domain('example.com')
Batch RDAP Queries (Python)
import time
from typing import List
from concurrent.futures import ThreadPoolExecutor, as_completed
def batch_query_domains(domains: List[str], max_workers=5):
"""
Query multiple domains with rate limiting and concurrency control
"""
client = RDAPClient(cache_ttl=3600)
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Submit all queries
future_to_domain = {
executor.submit(client.query_domain, domain): domain
for domain in domains
}
# Collect results as they complete
for future in as_completed(future_to_domain):
domain = future_to_domain[future]
try:
data = future.result()
results[domain] = client.parse_domain(data)
print(f"✓ Completed: {domain}")
except Exception as e:
results[domain] = {'error': str(e)}
print(f"✗ Failed: {domain} - {e}")
return results
# Usage
domains = [
'example.com',
'google.com',
'github.com',
'stackoverflow.com'
]
results = batch_query_domains(domains)
for domain, data in results.items():
if 'error' in data:
print(f"{domain}: Error - {data['error']}")
else:
print(f"{domain}: {', '.join(data['status'])}")
Understanding RDAP Responses
RDAP returns standardized JSON with consistent structure across all registries.
Complete RDAP Response Example
{
"objectClassName": "domain",
"handle": "EXAMPLE-COM",
"ldhName": "example.com",
"unicodeName": "example.com",
"status": [
"client delete prohibited",
"client transfer prohibited",
"client update prohibited"
],
"entities": [
{
"objectClassName": "entity",
"handle": "292",
"roles": ["registrar"],
"publicIds": [
{
"type": "IANA Registrar ID",
"identifier": "292"
}
],
"vcardArray": [
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "MarkMonitor Inc."]
]
]
}
],
"nameservers": [
{
"objectClassName": "nameserver",
"ldhName": "a.iana-servers.net",
"unicodeName": "a.iana-servers.net"
},
{
"objectClassName": "nameserver",
"ldhName": "b.iana-servers.net",
"unicodeName": "b.iana-servers.net"
}
],
"secureDNS": {
"delegationSigned": true,
"dsData": [
{
"keyTag": 31589,
"algorithm": 8,
"digest": "3490A6806D47F17A34C29E2CE80E8A999FFBE4BE",
"digestType": 1
}
]
},
"events": [
{
"eventAction": "registration",
"eventDate": "1995-08-14T04:00:00Z"
},
{
"eventAction": "expiration",
"eventDate": "2024-08-13T04:00:00Z"
},
{
"eventAction": "last changed",
"eventDate": "2023-08-14T07:01:38Z"
}
],
"links": [
{
"value": "https://rdap.verisign.com/com/v1/domain/EXAMPLE.COM",
"rel": "self",
"href": "https://rdap.verisign.com/com/v1/domain/EXAMPLE.COM",
"type": "application/rdap+json"
}
],
"port43": "whois.verisign-grs.com",
"rdapConformance": [
"rdap_level_0"
]
}
Key Response Fields
objectClassName:
"objectClassName": "domain"
Type of object returned (domain, entity, nameserver, etc.)
handle:
"handle": "EXAMPLE-COM"
Registry-assigned unique identifier
ldhName:
"ldhName": "example.com"
LDH (Letters, Digits, Hyphen) - ASCII domain name
unicodeName:
"unicodeName": "münchen.de"
Internationalized domain name (IDN) in Unicode
status:
"status": [
"client transfer prohibited",
"client update prohibited"
]
EPP status codes (same as WHOIS)
entities:
"entities": [
{
"roles": ["registrar"],
"vcardArray": [...]
}
]
Related entities (registrar, registrant, admin, tech contacts)
nameservers:
"nameservers": [
{
"ldhName": "ns1.example.com",
"ipAddresses": {
"v4": ["192.0.2.1"],
"v6": ["2001:db8::1"]
}
}
]
Authoritative nameservers with optional IP addresses
events:
"events": [
{
"eventAction": "registration",
"eventDate": "1995-08-14T04:00:00Z"
},
{
"eventAction": "expiration",
"eventDate": "2024-08-13T04:00:00Z"
}
]
Important dates (registration, expiration, last update)
secureDNS:
"secureDNS": {
"delegationSigned": true,
"dsData": [...]
}
DNSSEC information
Parsing RDAP Data
Extract useful information from RDAP responses.
Extract Domain Status
function extractStatus(rdapData) {
const status = rdapData.status || [];
return {
raw: status,
transferLocked: status.some(s => s.includes('transfer prohibited')),
updateLocked: status.some(s => s.includes('update prohibited')),
deleteLocked: status.some(s => s.includes('delete prohibited')),
onHold: status.some(s => s.includes('hold')),
pendingDelete: status.some(s => s.includes('pending delete')),
ok: status.includes('ok') || status.length === 0
};
}
Extract Dates
function extractDates(rdapData) {
const events = rdapData.events || [];
const dates = {};
events.forEach(event => {
const action = event.eventAction;
const date = new Date(event.eventDate);
if (action === 'registration') {
dates.created = date;
dates.age = Math.floor((Date.now() - date) / (1000 * 60 * 60 * 24)); // days
} else if (action === 'expiration') {
dates.expires = date;
dates.daysUntilExpiration = Math.floor((date - Date.now()) / (1000 * 60 * 60 * 24));
} else if (action === 'last changed') {
dates.updated = date;
}
});
return dates;
}
Extract Registrar
function extractRegistrar(rdapData) {
const entities = rdapData.entities || [];
const registrar = entities.find(entity =>
entity.roles && entity.roles.includes('registrar')
);
if (!registrar) return null;
let name = null;
let email = null;
let phone = null;
// Extract from vCard
if (registrar.vcardArray) {
registrar.vcardArray[1].forEach(vcard => {
if (vcard[0] === 'fn') name = vcard[3];
if (vcard[0] === 'email') email = vcard[3];
if (vcard[0] === 'tel') phone = vcard[3];
});
}
// Extract IANA ID
let ianaId = null;
if (registrar.publicIds) {
const ianaRecord = registrar.publicIds.find(id =>
id.type === 'IANA Registrar ID'
);
if (ianaRecord) ianaId = ianaRecord.identifier;
}
return { name, email, phone, ianaId };
}
Extract DNSSEC Status
function extractDNSSEC(rdapData) {
const secureDNS = rdapData.secureDNS || {};
return {
signed: secureDNS.delegationSigned || false,
dsRecords: (secureDNS.dsData || []).length,
details: secureDNS.dsData || []
};
}
Complete Parsing Function
function parseRDAPResponse(rdapData) {
return {
domain: rdapData.ldhName,
unicodeName: rdapData.unicodeName || rdapData.ldhName,
handle: rdapData.handle,
status: extractStatus(rdapData),
dates: extractDates(rdapData),
nameservers: (rdapData.nameservers || []).map(ns => ns.ldhName),
registrar: extractRegistrar(rdapData),
dnssec: extractDNSSEC(rdapData),
whoisServer: rdapData.port43,
raw: rdapData
};
}
Error Handling
Implement robust error handling for production use.
HTTP Status Codes
async function queryWithErrorHandling(domain) {
const url = `https://rdap.org/domain/${domain}`;
try {
const response = await fetch(url);
switch (response.status) {
case 200:
return await response.json();
case 404:
throw new Error('Domain not found');
case 429:
const retryAfter = response.headers.get('Retry-After');
throw new Error(`Rate limited. Retry after ${retryAfter} seconds`);
case 500:
throw new Error('RDAP server error');
case 503:
throw new Error('RDAP server unavailable');
default:
throw new Error(`Unexpected status: ${response.status}`);
}
} catch (error) {
if (error.message.includes('fetch')) {
throw new Error('Network error - check your connection');
}
throw error;
}
}
Retry Logic
async function queryWithRetry(domain, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await queryRDAP(domain);
} catch (error) {
lastError = error;
// Don't retry on 404 or validation errors
if (error.message.includes('not found') ||
error.message.includes('invalid')) {
throw error;
}
// Wait before retrying (exponential backoff)
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
Timeout Handling
async function queryWithTimeout(domain, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(`https://rdap.org/domain/${domain}`, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
Rate Limiting and Best Practices
Implementing Rate Limiting
class RateLimiter {
constructor(requestsPerSecond = 1) {
this.requestsPerSecond = requestsPerSecond;
this.lastRequest = 0;
}
async waitIfNeeded() {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequest;
const minInterval = 1000 / this.requestsPerSecond;
if (timeSinceLastRequest < minInterval) {
const waitTime = minInterval - timeSinceLastRequest;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.lastRequest = Date.now();
}
}
// Usage
const limiter = new RateLimiter(1); // 1 request per second
async function queryWithRateLimit(domain) {
await limiter.waitIfNeeded();
return await queryRDAP(domain);
}
Best Practices
-
Respect rate limits
- Limit to 1-2 requests per second
- Implement exponential backoff on errors
- Honor Retry-After headers
-
Cache responses
- Cache for at least 1 hour
- Respect TTL if provided
- Clear cache periodically
-
Set appropriate User-Agent
MyApp/1.0 ([email protected])Include app name, version, and contact info
-
Handle errors gracefully
- Don't retry on 404
- Implement backoff on 429/500/503
- Log errors for debugging
-
Use HTTPS
- Always use HTTPS (never HTTP)
- Validate SSL certificates
-
Timeout requests
- Set 10-30 second timeout
- Don't wait indefinitely
-
Query correct server
- Use direct registry servers for production
- Bootstrap service okay for low volume
Advanced RDAP Queries
Querying Entities (Registrars)
async function queryEntity(entityId) {
const url = `https://rdap.org/entity/${entityId}`;
const response = await fetch(url);
return await response.json();
}
// Example: Query registrar by IANA ID
const registrar = await queryEntity('292'); // MarkMonitor
Querying Nameservers
async function queryNameserver(nameserver) {
const url = `https://rdap.org/nameserver/${nameserver}`;
const response = await fetch(url);
return await response.json();
}
// Example
const ns = await queryNameserver('ns1.google.com');
Querying IP Addresses
async function queryIP(ipAddress) {
const url = `https://rdap.org/ip/${ipAddress}`;
const response = await fetch(url);
return await response.json();
}
// Example
const ipInfo = await queryIP('8.8.8.8');
Building an RDAP Client Library
Design Considerations
- Modular architecture
- Caching layer
- Rate limiting
- Error handling
- Retry logic
- Response parsing
- Batch processing
Example Library Structure
rdap-client/
├── src/
│ ├── client.js # Main client class
│ ├── cache.js # Caching implementation
│ ├── ratelimit.js # Rate limiting
│ ├── parser.js # Response parsing
│ ├── errors.js # Custom error classes
│ └── bootstrap.js # RDAP server discovery
├── test/
│ ├── client.test.js
│ ├── parser.test.js
│ └── fixtures/
└── examples/
├── basic.js
├── batch.js
└── advanced.js
RDAP vs WHOIS for Developers
Comparison for API Integration
| Feature | RDAP | WHOIS |
|---|---|---|
| Protocol | HTTP/HTTPS | TCP port 43 |
| Response Format | JSON | Plain text |
| Parsing | Native JSON | Custom parser per registry |
| Internationalization | Full Unicode | Limited/None |
| Rate Limiting | HTTP 429 + headers | Varies by server |
| Authentication | HTTP auth (planned) | None |
| Error Handling | HTTP status codes | Plain text messages |
| Caching | HTTP cache headers | Manual |
| Standardization | IETF RFC 9082-9083 | No standard format |
| Redirects | HTTP 301/302 | Manual lookup |
When to Use RDAP
✅ Use RDAP when:
- Building new applications
- Need structured data
- Require internationalization
- Want standard error handling
- Need HTTP/HTTPS integration
- Building RESTful APIs
- Modern web/mobile apps
When to Use WHOIS
⚠️ Use WHOIS when:
- Maintaining legacy systems
- RDAP not available for specific TLD
- Need raw WHOIS server response
- Historical compatibility required
Common Use Cases
1. Domain Availability Checker
async function isDomainAvailable(domain) {
try {
const data = await queryRDAP(domain);
return false; // Domain exists
} catch (error) {
if (error.message.includes('not found')) {
return true; // Domain available
}
throw error; // Other error
}
}
2. Domain Expiration Monitor
async function checkExpirationStatus(domain) {
const data = await queryRDAP(domain);
const events = data.events || [];
const expiration = events.find(e => e.eventAction === 'expiration');
if (!expiration) return null;
const expiryDate = new Date(expiration.eventDate);
const daysUntil = Math.floor((expiryDate - Date.now()) / (1000 * 60 * 60 * 24));
return {
domain,
expiryDate,
daysUntilExpiration: daysUntil,
status: daysUntil < 30 ? 'expiring-soon' : 'active'
};
}
3. Bulk Domain Audit
async function auditDomains(domains) {
const results = [];
for (const domain of domains) {
try {
const data = await queryRDAP(domain);
const parsed = parseDomain(data);
results.push({
domain,
status: 'success',
data: parsed
});
} catch (error) {
results.push({
domain,
status: 'error',
error: error.message
});
}
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 1000));
}
return results;
}
Frequently Asked Questions
Is RDAP free to use?
Yes, RDAP queries are free for basic lookups. Registries provide RDAP servers at no cost. However, rate limiting applies to prevent abuse. For high-volume commercial use, some registries may offer paid tiers with higher rate limits.
What rate limits apply to RDAP queries?
Rate limits vary by registry, but typical limits are 30-100 requests per minute for anonymous queries. Bootstrap services like rdap.org may have stricter limits. Always implement rate limiting in your code (1-2 requests per second is safe) and respect HTTP 429 responses.
Can I query historical RDAP data?
No, RDAP returns current data only. For historical WHOIS/RDAP data, use third-party services like DomainTools, WhoisXML API, or SecurityTrails that maintain historical databases.
How do I handle domains that don't have RDAP?
Not all TLDs have RDAP servers yet. If RDAP returns errors, fall back to WHOIS protocol. Check IANA's dns.json bootstrap file for current RDAP coverage. Most major TLDs (.com, .net, .org) support RDAP.
Can I get registrant email/phone from RDAP?
Due to GDPR and privacy regulations, most registries redact registrant contact information in RDAP responses. You may see "REDACTED FOR PRIVACY" or similar messages. Some registries provide proxy contact methods instead of direct contact info.
What's the difference between rdap.org and direct registry queries?
rdap.org is a bootstrap service that redirects to the appropriate registry RDAP server. Direct queries are faster (no redirect) and more reliable for production use, but require TLD-specific logic. rdap.org is convenient for development and low-volume use.
How do I query IDN (international) domains?
Use the punycode (ASCII-encoded) version of IDN domains in queries. For example, query xn--mnchen-3ya.de for münchen.de. The RDAP response will include both the ASCII (ldhName) and Unicode (unicodeName) versions.
Can I use RDAP for bulk domain lookups?
Yes, but implement proper rate limiting (1-2 requests per second), caching, and error handling. For very large bulk operations (100,000+ domains), consider using commercial WHOIS/RDAP data feeds or APIs specifically designed for bulk access.
Do I need authentication for RDAP queries?
Currently, most RDAP queries don't require authentication. RDAP specifications include authentication support for accessing restricted data, but this is not widely implemented yet. Public domain information is accessible without authentication.
How do I troubleshoot RDAP query failures?
Check: 1) Query the correct RDAP server for the TLD, 2) Verify domain exists (404 is normal for non-existent domains), 3) Check rate limiting (HTTP 429), 4) Test with curl/browser to isolate code issues, 5) Review RDAP server status pages for outages.
Key Takeaways
- RDAP returns JSON - Structured, machine-readable data vs unstructured WHOIS text
- Use rdap.org for simple queries - Bootstrap service handles TLD routing automatically
- Query registries directly for production - Faster and more reliable than bootstrap
- Implement rate limiting - Limit to 1-2 requests per second to avoid blocking
- Cache responses - Cache for at least 1 hour to reduce load
- Handle errors properly - Use HTTP status codes for error handling
- Set User-Agent header - Include app name and contact information
- Parse dates consistently - Events use ISO 8601 format
- RDAP is replacing WHOIS - Invest in RDAP for new development
- Privacy affects data availability - Contact info often redacted due to GDPR
- JavaScript and Python examples provided - Copy-paste ready code
- Timeout requests - Set 10-30 second timeouts to prevent hanging
Next Steps
Start Building with RDAP
-
Test Basic Queries
- Try examples from this guide
- Query rdap.org for test domains
- Examine JSON responses
- Understand response structure
-
Implement in Your Application
- Choose JavaScript or Python examples
- Add error handling
- Implement rate limiting
- Add caching layer
-
Build Production Client
- Query registries directly
- Implement retry logic
- Add comprehensive error handling
- Create parsing utilities
Explore RDAP Further
-
Read RDAP Specifications
- RFC 9082 - HTTP Usage
- RFC 9083 - JSON Responses
- RFC 9224 - Federated Authentication
-
Test Different TLDs
- Compare response formats
- Identify registry differences
- Document edge cases
-
Build Advanced Features
- Batch processing
- Historical tracking
- Monitoring systems
- Domain portfolio management
Use DomainDetails for RDAP Data
DomainDetails.com provides clean, parsed RDAP data:
- No coding required - Web interface for quick lookups
- Parsed JSON - Clean, structured data ready to use
- Bulk lookups - Check multiple domains at once (Pro)
- Historical tracking - Monitor changes over time (Pro)
- API access - Programmatic access to RDAP data (Pro)
Try DomainDetails → Upgrade to Pro →
Related Articles
- What is RDAP and How Does It Work?
- RDAP vs WHOIS: Complete Comparison
- DNS Record Types Explained
- Understanding EPP Status Codes
Research Sources
- RFC 9082 - Registration Data Access Protocol (RDAP) Query Format
- RFC 9083 - JSON Responses for the Registration Data Access Protocol (RDAP)
- RFC 9224 - Finding the Authoritative RDAP Service
- IANA - RDAP Bootstrap Service Registry
- ICANN - RDAP Technical Implementation Guide
- Verisign - RDAP Developer Documentation
- APNIC - RDAP Implementation Guide
- RIPE NCC - RDAP Query Examples
- ARIN - RDAP Web Service Documentation
- IETF REGEXT Working Group - RDAP Protocol Extensions