To query RDAP from PHP, download the IANA bootstrap file at data.iana.org/rdap/dns.json, look up the RDAP base URL for the domain's TLD and send GET <base>/domain/<name> over HTTPS. The response is JSON defined in RFC 9083. HTTP 404 normally means the domain isn't registered. Some ccTLDs are missing from the bootstrap and need a hard-coded URL.
Whois output is a parsing exercise with a different answer for every registry. RDAP fixes that by returning JSON, but it comes with its own surprises: a discovery step most tutorials skip, ccTLDs that aren’t in the discovery file, and at least one major registry whose TLS behaviour trips up PHP’s cURL on every “not found”.
RDAP versus whois in practice
RDAP, the Registration Data Access Protocol, is defined in RFC 9082 (queries) and RFC 9083 (responses). It runs over HTTPS and returns structured data, which changes how you write the client:
| Whois (port 43) | RDAP | |
|---|---|---|
| Transport | raw TCP | HTTPS |
| Format | free text per registry | JSON with standard members |
| Finding the server | convention or whois.iana.org | IANA bootstrap file |
| Not found | phrases like “No match for” | HTTP 404 |
| Internationalised names | inconsistent | punycode in URL, unicodeName in JSON |
For anything under ICANN contract, RDAP is now the primary interface. The whois guide covers the fallback you still need for many ccTLDs.
Finding the right server with the RDAP bootstrap
IANA publishes https://data.iana.org/rdap/dns.json, mapping TLDs to RDAP base URLs. Its services array holds pairs of TLD lists and URL lists:
{
"version": "1.0",
"publication": "2026-09-30T23:00:03Z",
"services": [
[["com"], ["https://rdap.verisign.com/com/v1/"]],
[["uk"], ["https://rdap.nominet.uk/uk/"]]
]
}
The copy I fetched for this guide had 592 service entries. More telling is what was absent: .io, .co, .me, .eu, .de and .ch were not listed. For those you either know the URL yourself or fall back to whois. Don’t assume the bootstrap is complete.
A tested RDAP client for PHP 8.2+
The class below caches the bootstrap for a day, resolves the server per TLD and returns the decoded JSON, or null for a 404. I ran it on PHP 8.5 against Verisign, Public Interest Registry and Nominet.
<?php
declare(strict_types=1);
final class RdapClient
{
private const BOOTSTRAP = 'https://data.iana.org/rdap/dns.json';
private const FALLBACK = []; // e.g. ['xy' => 'https://rdap.example-registry.xy/']
/** @var array<string, string>|null */
private ?array $map = null;
public function __construct(private string $cacheFile = '/tmp/rdap-dns.json') {}
public function baseUrl(string $tld): ?string
{
if ($this->map === null) {
$this->map = $this->loadBootstrap();
}
return $this->map[$tld] ?? self::FALLBACK[$tld] ?? null;
}
/** @return array<string, string> */
private function loadBootstrap(): array
{
$fresh = is_file($this->cacheFile) && filemtime($this->cacheFile) > time() - 86400;
$json = $fresh ? file_get_contents($this->cacheFile) : $this->get(self::BOOTSTRAP)[1];
if ($fresh === false) {
file_put_contents($this->cacheFile, $json);
}
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
$map = [];
foreach ($data['services'] as [$tlds, $urls]) {
// prefer https when several URLs are listed
$https = array_values(array_filter($urls, fn(string $u) => str_starts_with($u, 'https://')));
$url = rtrim($https[0] ?? $urls[0], '/') . '/';
foreach ($tlds as $tld) {
$map[strtolower($tld)] = $url;
}
}
return $map;
}
/** @return array{0:int,1:string} */
private function get(string $url): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Accept: application/rdap+json'],
CURLOPT_USERAGENT => 'rdap-php-example/1.0',
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
// Some servers drop the TLS connection after a 404 without close_notify.
// $body is false then, but the status code is valid.
if ($body === false && $status === 0) {
throw new RuntimeException(curl_error($ch));
}
return [$status, (string) $body];
}
public function domain(string $domain): ?array
{
$ascii = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
if ($ascii === false) {
throw new InvalidArgumentException("Not a valid domain name: $domain");
}
$tld = substr(strrchr($ascii, '.'), 1);
$base = $this->baseUrl($tld) ?? throw new RuntimeException("No RDAP server known for .$tld");
[$status, $body] = $this->get($base . 'domain/' . rawurlencode($ascii));
return match (true) {
$status === 404 => null, // not registered (usually)
$status === 429 => throw new RuntimeException('RDAP server rate limit'),
$status >= 400 => throw new RuntimeException("RDAP error HTTP $status"),
default => json_decode($body, true, 512, JSON_THROW_ON_ERROR),
};
}
}
That odd check in get() comes from testing. Querying rdap.verisign.com for a .com that doesn’t exist, cURL reported status 404 but curl_exec() returned false with:
OpenSSL SSL_read: OpenSSL/4.0.3: error:0A000126:SSL routines::unexpected eof while reading, errno 0
The server closes the socket without a proper TLS shutdown and OpenSSL 3 and later treat that as an error. If your client only checks $body === false, every unregistered .com becomes a network failure.
Reading the RDAP response
You rarely need the whole document. Status, registration and expiry dates, nameservers and the registrar cover most use cases. Dates live in events; the registrar is an entity with the role registrar and a jCard in vcardArray.
<?php
declare(strict_types=1);
function summarize(array $rdap): array
{
$events = [];
foreach ($rdap['events'] ?? [] as $e) {
$events[$e['eventAction']] = $e['eventDate'];
}
$registrar = null;
foreach ($rdap['entities'] ?? [] as $entity) {
if (in_array('registrar', $entity['roles'] ?? [], true)) {
$registrar = $entity['handle'] ?? null;
foreach ($entity['vcardArray'][1] ?? [] as $prop) {
if ($prop[0] === 'fn') {
$registrar = $prop[3];
}
}
}
}
return [
'name' => $rdap['ldhName'] ?? null,
'status' => $rdap['status'] ?? [],
'registered' => $events['registration'] ?? null,
'expires' => $events['expiration'] ?? null,
'nameservers' => array_map(fn(array $ns) => rtrim(strtolower($ns['ldhName']), '.'), $rdap['nameservers'] ?? []),
'registrar' => $registrar,
];
}
For example.com the summary came back as:
{
"name": "EXAMPLE.COM",
"status": ["client delete prohibited", "client transfer prohibited", "client update prohibited"],
"registered": "1995-08-14T04:00:00Z",
"expires": "2027-08-13T04:00:00Z",
"nameservers": ["elliott.ns.cloudflare.com", "hera.ns.cloudflare.com"],
"registrar": "RESERVED-Internet Assigned Numbers Authority"
}
Some registries omit dates entirely or write nameservers with a trailing dot, hence the rtrim(). Never assume a field is present.
Status values worth knowing
Status strings follow RFC 8056, which maps EPP status codes to RDAP. They’re lowercase with spaces, so client transfer prohibited rather than whois’s clientTransferProhibited.
| RDAP status | Meaning |
|---|---|
active | registered, no locks |
client transfer prohibited | registrar transfer lock, normal |
server hold | pulled from the zone by the registry, won’t resolve |
redemption period | expired, owner can still restore it |
pending delete | about to be deleted and released |
An availability tool that only looks for 404 misses the last two, even though they’re exactly what drop-catchers care about.
What a 404 does and doesn’t tell you
RFC 9082 uses 404 for “object not found”. It’s a strong hint that a domain is free, not a guarantee: reserved names and registry premium names can return 404 and still be unregistrable. The guide on checking domain availability in PHP shows how to confirm with a registrar.
Rate limits are rarely documented. Assume a server will start answering 429, or simply hang, after a few dozen requests a minute. Send a descriptive User-Agent, cache answers for at least an hour and keep timeouts short so a slow registry can’t tie up your PHP-FPM workers.
Beyond domain lookups
RFC 9082 defines more paths than /domain/. On the same base URL you can ask for /nameserver/<host> to see which IP addresses a registry has stored as glue, or /entity/<handle> to resolve a registrar handle into its contact details. Support varies: some registries implement only domain queries and answer everything else with 404 or 501. Treat those extra endpoints as a bonus, not a contract.
IP addresses and autonomous systems use separate bootstrap files at the same IANA location (ipv4.json, ipv6.json, asn.json). The structure is identical, so the class above works with a different bootstrap URL if you need to find out which regional registry an address belongs to.
One more habit worth adopting: log the rdapConformance array the first time you talk to a new server. It lists the extensions the registry supports, and when a response looks strange, that list is usually where the explanation starts.
Skipping the plumbing
If non-personal registry data is all you need, betadomain’s MCP server offers a whois_lookup tool that runs this flow server-side, bootstrap and punycode included, and returns status, dates, nameservers and registrar. How to call it from PHP or an AI client is covered in MCP server domain tools and the betadomain API reference. Internationalised names need converting first, as the punycode guide explains.
Got a 404 for the name you wanted? Confirm it with the betadomain availability search and compare the renewal price on the .com extension page, currently €14.99 a year.
Step by step
- Fetch and cache the bootstrapDownload data.iana.org/rdap/dns.json, cache it for 24 hours and turn it into a TLD to base URL map.
- Convert the domain to punycodeRun idn_to_ascii() and split off the TLD.
- Query the RDAP serverSend GET <base>/domain/<ascii-domain> with Accept: application/rdap+json.
- Interpret the responseTreat 404 as not registered, 429 as rate-limited, and otherwise parse events, status and nameservers from the JSON.
Found your domain? You can check and register it directly at domains at a fixed yearly price – yearly price from €2.99.
FAQ
Has RDAP replaced whois?
For gTLDs, largely yes. ICANN has required RDAP from gTLD registries for years, and since 28 January 2025 they and registrars are no longer obliged to run port 43 whois. Country-code TLDs set their own rules, so many still offer only whois.
Why don't I see registrant names in RDAP responses?
Since GDPR most registries and registrars redact personal data. You'll typically get status, dates, nameservers and the registrar, with contact entities either missing or replaced by redaction notices. Access to the underlying data requires a legitimate request through the registrar.
How often should I refresh the bootstrap file?
Once a day is fine. It changes rarely and carries a publication timestamp. Fetching it on every lookup slows your code down and puts needless load on IANA.
Is there a Composer package for RDAP?
The protocol is plain HTTPS and JSON, so a small cURL class covers most needs. If you also want whois parsing, io-developer/php-whois on Packagist handles both and is actively maintained.