PHP: Check Domain Availability the Right Way

Updated: 5 min read Editorial team The PHP Experts
Short answer

To check domain availability in PHP reliably you have to ask the registry or a registrar, via RDAP, whois or a registrar API. A DNS lookup with checkdnsrr() only works as a pre-filter: if the domain has NS records it is taken, but a domain without them can still be registered, reserved or pending deletion.

Most domain checkers I’ve reviewed start with gethostbyname(). If it returns the input string unchanged, the name “doesn’t resolve”, so the code reports it as free. It works on the developer’s laptop and then tells a customer that a parked, unconfigured domain is up for grabs.

Where does the truth about availability live?

Only the registry behind an extension knows for sure whether a name is registered: Verisign for .com and .net, Public Interest Registry for .org, Google Registry for .dev. Every other source is a copy or an approximation. When you check domain availability in PHP you can pick from four approaches:

MethodHit meansNo hit meansEffort
DNS (checkdnsrr)definitely takenuncleartrivial
RDAP (HTTPS + JSON)definitely takenusually free (HTTP 404)moderate, needs bootstrap
Whois (port 43)definitely takenusually free, wording varieshigh, per-registry parsing
Registrar APItaken / freetaken / freelow, but rate-limited

A registrar API is the only one that asks the question the way an actual registration would, so it catches reserved and premium names that RDAP happily reports as “not found”.

Why DNS lookups produce false “available” results

DNS answers a different question: are there records for this name right now? A domain can be registered with no nameservers configured, be locked in a dispute, or sit in the middle of a transfer. None of those will resolve.

The reverse is reliable, though. If a name has NS or SOA records, somebody owns it. That makes DNS a good first pass when you’re checking dozens of suggestions and want to spare the rate-limited API.

<?php
declare(strict_types=1);

function hasDnsFootprint(string $domain): bool
{
    // NS or SOA present: the domain is registered
    return checkdnsrr($domain, 'NS') || checkdnsrr($domain, 'SOA');
}

For the details of checkdnsrr() and dns_get_record(), including the warnings they throw on resolver failures, see the guide to PHP dns_get_record.

Checking availability through an API

betadomain publishes two read-only endpoints that need no key and no account. The developer documentation describes them, and there is an OpenAPI 3.1 file at /openapi.json if you want to generate a client.

<?php
declare(strict_types=1);

function checkViaBetadomain(string $domain): array
{
    $ch = curl_init('https://betadomain.de/api/domain/check');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode(['domain' => $domain], JSON_THROW_ON_ERROR),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
    ]);
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

    if ($body === false) {
        throw new RuntimeException('Network error: ' . curl_error($ch));
    }
    if ($status === 429) {
        throw new RuntimeException('Rate limit hit, retry later');
    }
    if ($status !== 200) {
        throw new RuntimeException("HTTP $status: $body");
    }

    return json_decode($body, true, 512, JSON_THROW_ON_ERROR);
}

The response carries available plus annual prices for registration, transfer and renewal (registerEur, transferEur, renewEur). A .com currently costs €14.99 a year there and renews at the same price. If the extension isn’t one betadomain sells, you get {"supported": false} instead of a guess.

Checking a name across several extensions

POST /api/domain/search with {"query": "myproject"} checks .de, .com, .eu, .net, .org, .shop, .io and .dev in parallel and returns a results array with domain, available and priceEur per entry. One request instead of eight is a real saving when you’re rate-limited.

Putting it together with a three-state result

The function below returns true for available, false for taken and null for “don’t know”. That third state matters more than people think: treating a timeout as “available” is how users end up at a checkout that fails.

<?php
declare(strict_types=1);

function isAvailable(string $domain): ?bool
{
    $ascii = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
    if ($ascii === false) {
        throw new InvalidArgumentException("Invalid domain name: $domain");
    }
    if (hasDnsFootprint($ascii)) {
        return false; // no API call needed
    }
    $result = checkViaBetadomain($ascii);
    if (($result['supported'] ?? true) === false) {
        return null; // extension not offered
    }
    return $result['available'] ?? null;
}

foreach (['php.net', 'my-demo-project-4711.com', 'bücher-demo-4711.com'] as $d) {
    var_dump($d, isAvailable($d));
}

I ran this against the live endpoint on PHP 8.5: php.net is caught by the DNS step, the two invented names come back as available. The internationalised one goes through punycode conversion first; the punycode guide explains why the non-transitional flag is the one you want.

Rate limits you need to design for

The public endpoints allow 30 requests per minute and 300 per six hours per IP address. Exceed that and you get HTTP 429 with a Retry-After header in seconds. For a form on your site that’s plenty. For a nightly job checking a dictionary, it isn’t, and it isn’t meant to be.

What has worked well for me:

  • Cache results. “Taken” can live for a day; “available” should expire within minutes.
  • Debounce input in the browser by 300 to 500 ms so each keystroke doesn’t fire a request.
  • Run the check on your server. If you call the API from users’ browsers, an entire office behind one NAT address shares a single quota.
  • On 429, wait for the number of seconds in Retry-After.

If you’d rather query registries directly, RDAP in PHP is the modern route, and whois over port 43 still works for most ccTLDs. Both come with their own limits, which registries rarely publish.

What no availability check can promise

Every answer is a snapshot. Between “available” and the click on “register”, someone else can take the name. Some states aren’t visible through any public interface at all:

  • premium names priced by the registry, not the registrar
  • reserved strings such as country names on certain gTLDs
  • domains in redemption or pending delete after expiry
  • extensions with eligibility rules, like local presence

So put a line in your UI saying availability is confirmed at registration. It’s not legal padding, it’s how the system works.

If you just want to know whether your project name is still free, run it through the betadomain domain search and compare renewal prices on the full price list before you commit.

Step by step

  1. Normalise the inputTrim, lowercase and convert to punycode with idn_to_ascii(). Reject the input if the conversion fails.
  2. Use DNS as a cheap filterQuery NS and SOA records with checkdnsrr(). If either exists, the domain is taken and no further lookup is needed.
  3. Ask an authoritative sourceFor the remaining names call RDAP, whois or a registrar API such as POST /api/domain/check.
  4. Handle limits and errorsHonour HTTP 429 and Retry-After, and report network failures as unknown rather than available.

Found your domain? You can check and register it directly at buy a domain – yearly price from €2.99.

FAQ

Is gethostbyname() enough to see whether a domain is free?

No. gethostbyname() only tells you whether an A record resolves. Plenty of registered domains have no A record, and some have no nameservers at all. Use it, or better checkdnsrr() with NS, only to rule names out.

Is there a free domain availability API?

betadomain exposes POST /api/domain/check without an API key. It is rate-limited per IP to 30 requests per minute and 300 per six hours, which suits signup forms and name pickers. Bulk scanning of large lists is outside its fair-use scope.

Why does a domain show as available but fail at registration?

Registries hold back premium names, reserved strings and recently deleted domains. Whois and RDAP don't always reflect those states. The registrar only finds out for certain when it attempts the registration.

How do I check internationalised names like bücher.com?

Convert them to punycode first with idn_to_ascii(), which turns bücher.com into xn--bcher-kva.com. DNS, whois and RDAP all expect that ASCII form.