WHOIS to RDAP Migration Guide with Code Examples
February 25, 2026
On January 28, 2025, ICANN officially sunsetted WHOIS for gTLD registries and registrars. The 2023 Global Amendments to the Registry Agreement removed the contractual obligation to run port 43 — registries can now shut it off at any time.
Port 43 still works at most registries today. But there's no guarantee it will tomorrow. If your code depends on WHOIS, here's how to migrate.
Step 1: Find your WHOIS code
Search your codebase for these common WHOIS libraries and patterns:
| Language | What to grep for |
|---|---|
| Python | python-whois, import whois, whois.whois( |
| Node.js | whois (npm), whoiser, node-whois |
| PHP | phpwhois/phpwhois, io-developer/php-whois |
| Go | likexian/whois, domainr/whois |
| Java | commons-net (Apache), org.apache.commons.net.whois |
| Any | port 43, whois., subprocess calls to whois |
List the fields you actually use. Most teams only need three or four: expiry date, registrar name, nameservers, maybe registrant organization.
Step 2: Understand raw RDAP
RDAP is just HTTPS + JSON. Here's a direct query to Verisign's RDAP server:
curl https://rdap.verisign.com/com/v1/domain/google.com
The response is structured — no parsing needed for basic fields like nameservers, status, and dates. But contact data lives on the registrar's RDAP server (not the registry), and uses vcardArray (RFC 6350), which looks like this:
[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "REDACTED REGISTRANT"],
["org", {"type": "work"}, "text", "Google LLC"],
["adr", {"cc": "US"}, "text",
["", "", "REDACTED FOR PRIVACY", "REDACTED FOR PRIVACY", "", "REDACTED FOR PRIVACY", ""]
],
["tel", {"type": "voice"}, "text", "REDACTED FOR PRIVACY"],
["email", {}, "text", "REDACTED FOR PRIVACY"]
]
]
This is the actual registrant vcardArray from MarkMonitor's RDAP server for google.com. To extract the organization name, you iterate the inner array, find the element where index 0 is "org", and read index 3. Most fields are redacted — and you only get vcardArray after following the registrar link from the registry response (see "Thin registries" below).
And that's just one problem. Here's what else you'll hit querying RDAP directly:
Gotcha: Finding the right server
There's no single RDAP server. You need IANA's bootstrap registry to discover which server handles each TLD — an extra HTTP call, cached and refreshed periodically.
Gotcha: Thin registries
.com and .net are "thin" registries — Verisign stores the basics (nameservers, status, dates), but registrant details, abuse contacts, and richer registrar data live on the registrar's separate RDAP server. The registry response includes a links array with the registrar's URL. You need a second request and a merge step.
Gotcha: ccTLDs without RDAP
Not all country-code TLDs support RDAP. .de and .uk do. Others are still WHOIS-only. Check the IANA bootstrap file — if a TLD isn't listed, there's no RDAP server for it. Your migration needs a fallback strategy or you accept gaps for those TLDs.
Gotcha: Rate limiting
Each upstream RDAP server enforces its own rate limits with different thresholds. You'll get 429 responses with varying Retry-After headers, and there's no central documentation on limits.
Gotcha: Inconsistent optional fields
The RFC defines the structure, but registries differ on which optional fields they include, how deeply they nest entities, and what status values they return. A field present in one registry's response may be absent or structured differently in another's.
Step 3: Migrate your code
Here's the before/after. Replace WHOIS regex with structured JSON access.
Before: WHOIS parsing
import whois # pip install python-whois
w = whois.whois("google.com")
print(w.expiration_date) # datetime or list — varies by TLD
print(w.registrar) # "MarkMonitor Inc." (or None)
print(w.name_servers) # frozenset — sometimes list, sometimes None
const whois = require("whois"); // npm install whois
whois.lookup("google.com", (err, data) => {
// `data` is raw text — you still need regex
const expiry = data.match(/Expir.*?:\s*(.+)/)?.[1];
const registrar = data.match(/Registrar:\s*(.+)/)?.[1];
});
// Uses likexian/whois + likexian/whois-parser
raw, _ := whois.Whois("google.com")
result, _ := whoisparser.Parse(raw)
fmt.Println(result.Domain.ExpirationDate) // string, format varies
fmt.Println(result.Registrar.Name)
// Uses io-developer/php-whois
$whois = \Iodev\Whois\Factory::get()->createWhois();
$info = $whois->loadDomainInfo("google.com");
echo $info->expirationDate; // Unix timestamp or null
echo $info->registrar;
// Uses Apache Commons Net
import org.apache.commons.net.whois.WhoisClient;
WhoisClient whois = new WhoisClient();
whois.connect("whois.verisign-grs.com");
String result = whois.query("google.com"); // raw text — parse it yourself
whois.disconnect();
// No built-in parser — regex or manual string splitting required
This works for .com. It breaks differently for .de (German field labels), .jp (different date format), .uk (different structure entirely), and many others. Each library handles these inconsistencies differently — or doesn't handle them at all.
After: RDAP (direct)
You can query RDAP servers directly — it's free and works today:
import requests
# Step 1: bootstrap — find the right RDAP server for .com
bootstrap = requests.get("https://data.iana.org/rdap/dns.json").json()
rdap_base = None
for entry in bootstrap["services"]:
if "com" in entry[0]:
rdap_base = entry[1][0]
break
# In production, cache this and refresh daily
# Step 2: query the registry
resp = requests.get(f"{rdap_base}domain/google.com")
data = resp.json()
print(data["events"][0]["eventDate"]) # expiry — but which event is which?
print(data["nameservers"][0]["ldhName"])
# Step 3: follow registrar link for contact data (thin registry)
registrar_url = next(
link["href"] for link in data.get("links", [])
if link.get("rel") == "related"
)
registrar_resp = requests.get(registrar_url)
# Now merge the two responses and parse vcardArray...
// Step 1: bootstrap — find the right RDAP server for .com
const bootstrap = await fetch("https://data.iana.org/rdap/dns.json")
.then((r) => r.json());
const rdapBase = bootstrap.services
.find(([tlds]) => tlds.includes("com"))?.[1][0];
// In production, cache this and refresh daily
// Step 2: query the registry
const resp = await fetch(`${rdapBase}domain/google.com`);
const data = await resp.json();
console.log(data.events[0].eventDate); // expiry — but which event?
console.log(data.nameservers[0].ldhName);
// Step 3: follow registrar link for contact data (thin registry)
const registrarUrl = data.links?.find(
(l) => l.rel === "related"
)?.href;
const registrarResp = await fetch(registrarUrl);
// Now merge the two responses and parse vcardArray...
// Step 1: bootstrap — find the right RDAP server for .com
bootstrapResp, err := http.Get("https://data.iana.org/rdap/dns.json")
// handle err
defer bootstrapResp.Body.Close()
var bootstrap map[string]interface{}
json.NewDecoder(bootstrapResp.Body).Decode(&bootstrap)
// iterate bootstrap["services"] to find base URL for "com"
// In production, cache this and refresh daily
// Step 2: query the registry (rdapBase from bootstrap)
resp, err := http.Get(rdapBase + "domain/google.com")
// handle err
defer resp.Body.Close()
var data map[string]interface{}
json.NewDecoder(resp.Body).Decode(&data)
// Step 3: follow registrar link for contact data (thin registry)
// find rel=related in links array, fetch registrar URL, merge, parse vcardArray...
// Step 1: bootstrap — find the right RDAP server for .com
$bootstrap = json_decode(file_get_contents("https://data.iana.org/rdap/dns.json"), true);
$rdapBase = null;
foreach ($bootstrap['services'] as $entry) {
if (in_array('com', $entry[0])) { $rdapBase = $entry[1][0]; break; }
}
// In production, cache this and refresh daily
// Step 2: query the registry
$data = json_decode(file_get_contents("{$rdapBase}domain/google.com"), true);
echo $data['events'][0]['eventDate']; // expiry — but which event?
echo $data['nameservers'][0]['ldhName'];
// Step 3: follow registrar link for contact data (thin registry)
$registrarUrl = array_values(array_filter(
$data['links'], fn($l) => $l['rel'] === 'related'
))[0]['href'];
$registrarResp = file_get_contents($registrarUrl);
// Now merge the two responses and parse vcardArray...
// Step 1: bootstrap — find the right RDAP server for .com
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://data.iana.org/rdap/dns.json")).build();
HttpResponse<String> bootstrapResp = client.send(req, HttpResponse.BodyHandlers.ofString());
// parse JSON with Jackson/Gson, find base URL for "com"
// In production, cache this and refresh daily
// Step 2: query the registry (rdapBase from bootstrap)
req = HttpRequest.newBuilder()
.uri(URI.create(rdapBase + "domain/google.com")).build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// parse JSON with Jackson/Gson
// Step 3: follow registrar link for contact data (thin registry)
// find rel=related in links array, fetch registrar URL, merge, parse vcardArray...
This works, but you're handling bootstrap discovery, thin-registry follow-through, vcardArray parsing, rate limiting, and field inconsistencies yourself. For every TLD.
After: RDAP (normalized via SDK)
Or skip the plumbing with an SDK that handles it all:
import os
from rdapapi import RdapApi # pip install rdapapi
api = RdapApi(os.environ["RDAP_API_KEY"])
domain = api.domain("google.com", follow=True)
print(domain.dates.expires) # "2028-09-14T04:00:00Z"
print(domain.registrar.name) # "MarkMonitor Inc."
print(domain.nameservers) # ["ns1.google.com", ...]
print(domain.entities.registrant.organization) # "Google LLC"
import { RdapApi } from "rdapapi"; // npm install rdapapi
const api = new RdapApi(process.env.RDAP_API_KEY);
const domain = await api.domain("google.com", { follow: true });
console.log(domain.dates.expires); // "2028-09-14T04:00:00Z"
console.log(domain.registrar.name); // "MarkMonitor Inc."
console.log(domain.nameservers); // ["ns1.google.com", ...]
// go get github.com/rdapapi/rdapapi-go
client := rdapapi.NewClient(os.Getenv("RDAP_API_KEY"))
domain, err := client.Domain("google.com", &rdapapi.DomainOptions{Follow: true})
// handle err
fmt.Println(domain.Dates.Expires) // "2028-09-14T04:00:00Z"
fmt.Println(domain.Registrar.Name) // "MarkMonitor Inc."
fmt.Println(domain.Nameservers) // ["ns1.google.com", ...]
// composer require rdapapi/rdapapi-php (requires PHP 8.0+)
$api = new \RdapApi\RdapApi(getenv('RDAP_API_KEY'));
$domain = $api->domain('google.com', follow: true);
echo $domain->dates->expires; // "2028-09-14T04:00:00Z"
echo $domain->registrar->name; // "MarkMonitor Inc."
echo $domain->entities->registrant->organization; // "Google LLC"
// io.rdapapi:rdapapi-java (Maven Central)
RdapClient client = new RdapClient(System.getenv("RDAP_API_KEY"));
DomainResponse domain = client.domain("google.com",
new DomainOptions().follow(true));
System.out.println(domain.getDates().getExpires()); // "2028-09-14T04:00:00Z"
System.out.println(domain.getRegistrar().getName()); // "MarkMonitor Inc."
System.out.println(domain.getNameservers()); // [ns1.google.com, ...]
Same fields, same structure, every TLD. Bootstrap discovery, vcardArray parsing, thin-registry follow-through, and response normalization happen upstream.
Before you ship
- Test across TLD families — try
.de,.jp,.xyz, and a new gTLD like.app. If your new code works on all four, it's solid - Decide on ccTLD gaps — some ccTLDs have no RDAP server. Keep a WHOIS fallback for those, or accept missing coverage
- Monitor port 43 — as registries disable WHOIS, existing integrations will start returning empty responses or connection timeouts with no warning
Further reading
- RDAP vs WHOIS: What Developers Need to Know in 2026 — protocol comparison and technical backstory
- ICANN: Launching RDAP, Sunsetting WHOIS — the official announcement
- API documentation — interactive endpoint reference
- Start your free trial — get an API key after signing up