domaindetails.com
Knowledge Base/Technical Guides/How to Query RDAP Programmatically: Developer Guide (2025)
Technical Guides

How to Query RDAP Programmatically: Developer Guide (2025)

Complete developer guide to querying RDAP (Registration Data Access Protocol) programmatically. Includes JavaScript and Python code examples, rate limiting, error handling, and parsing.

13 min
Published 2025-12-01
Updated 2025-12-01
By DomainDetails Team

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 (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:

  1. Structured Data

    WHOIS: Unstructured text, varies by registry
    RDAP: Standardized JSON, consistent format
    
  2. Machine-Readable

    WHOIS: Requires custom parsing per registry
    RDAP: Native JSON parsing
    
  3. Better Internationalization

    WHOIS: ASCII only, poor i18n support
    RDAP: Full Unicode support, multiple languages
    
  4. Standard Error Handling

    WHOIS: Inconsistent error messages
    RDAP: Standard HTTP status codes + detailed errors
    
  5. 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

  1. Domain availability checking
  2. Domain portfolio management
  3. Security research and threat intelligence
  4. Compliance and monitoring tools
  5. Registrar/registry integrations
  6. Domain expiration monitoring
  7. 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 identifier
  • ldhName: ASCII domain name
  • status: EPP status codes
  • entities: Related entities (registrar, registrant)
  • nameservers: Authoritative nameservers
  • events: 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:

  1. Detects the TLD from your query
  2. Looks up the correct RDAP server
  3. Redirects your query to the appropriate registry
  4. 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:

  1. Download dns.json periodically (cache for 24h)
  2. Parse and build TLD → RDAP server mapping
  3. Look up server for queried domain's TLD
  4. 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

  1. Respect rate limits

    • Limit to 1-2 requests per second
    • Implement exponential backoff on errors
    • Honor Retry-After headers
  2. Cache responses

    • Cache for at least 1 hour
    • Respect TTL if provided
    • Clear cache periodically
  3. Set appropriate User-Agent

    MyApp/1.0 ([email protected])
    

    Include app name, version, and contact info

  4. Handle errors gracefully

    • Don't retry on 404
    • Implement backoff on 429/500/503
    • Log errors for debugging
  5. Use HTTPS

    • Always use HTTPS (never HTTP)
    • Validate SSL certificates
  6. Timeout requests

    • Set 10-30 second timeout
    • Don't wait indefinitely
  7. 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

  1. Modular architecture
  2. Caching layer
  3. Rate limiting
  4. Error handling
  5. Retry logic
  6. Response parsing
  7. 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

  1. Test Basic Queries

    • Try examples from this guide
    • Query rdap.org for test domains
    • Examine JSON responses
    • Understand response structure
  2. Implement in Your Application

    • Choose JavaScript or Python examples
    • Add error handling
    • Implement rate limiting
    • Add caching layer
  3. Build Production Client

    • Query registries directly
    • Implement retry logic
    • Add comprehensive error handling
    • Create parsing utilities

Explore RDAP Further

  1. Read RDAP Specifications

    • RFC 9082 - HTTP Usage
    • RFC 9083 - JSON Responses
    • RFC 9224 - Federated Authentication
  2. Test Different TLDs

    • Compare response formats
    • Identify registry differences
    • Document edge cases
  3. 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 →

Research Sources

  1. RFC 9082 - Registration Data Access Protocol (RDAP) Query Format
  2. RFC 9083 - JSON Responses for the Registration Data Access Protocol (RDAP)
  3. RFC 9224 - Finding the Authoritative RDAP Service
  4. IANA - RDAP Bootstrap Service Registry
  5. ICANN - RDAP Technical Implementation Guide
  6. Verisign - RDAP Developer Documentation
  7. APNIC - RDAP Implementation Guide
  8. RIPE NCC - RDAP Query Examples
  9. ARIN - RDAP Web Service Documentation
  10. IETF REGEXT Working Group - RDAP Protocol Extensions