Engineering

How to Query WHOIS Data in PHP Using a REST API (2026 Guide)

September 21, 202610 min readPHP · WHOIS · REST API · cURL

Introduction

PHP still powers a large share of SaaS backends, WordPress plugins, Laravel apps and billing portals. Domain intelligence fits naturally into those stacks: verify a customer’s domain at signup, check expiry before renewals, or enrich abuse tickets with registrar and EPP status — without shelling out to a system whois binary.

Parsing legacy WHOIS text in PHP is fragile: field names differ by TLD, privacy proxies redact contacts, and error handling is inconsistent. A REST WHOIS API returns normalized JSON. This guide shows how to integrate the WhoisJSON WHOIS API into PHP — from a reusable cURL client to HTTP 429 retries andcurl_multi bulk lookups.

Companion guides already cover Python, Node.js and Go. The endpoint and response schema are identical across languages.

Prerequisites

  • PHP 8.1+ — examples use typed properties and named arguments where helpful. PHP 7.4 works with minor syntax tweaks.
  • ext-curl enabled — required for the standard-library examples (php -m | grep curl).
  • A free WhoisJSON API key sign up here for 1,000 free requests per month. No credit card required.
  • Optional:guzzlehttp/guzzle — if you already use Guzzle in Laravel or Symfony.

Basic WHOIS Lookup in PHP

Authenticate withAuthorization: TOKEN=YOUR_API_KEY. The base URL ishttps://whoisjson.com/api/v1.

WhoisClient.phpPHP
<?php

declare(strict_types=1);

final class WhoisClient
{
    private const BASE_URL = 'https://whoisjson.com/api/v1';

    public function __construct(
        private readonly string $apiKey,
        private readonly int $timeoutSeconds = 15,
    ) {}

    public function lookup(string $domain, bool $forceRefresh = false): array
    {
        $query = ['domain' => $domain];
        if ($forceRefresh) {
            $query['_forceRefresh'] = 1;
        }

        $url = self::BASE_URL . '/whois?' . http_build_query($query);

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->timeoutSeconds,
            CURLOPT_HTTPHEADER => [
                'Authorization: TOKEN=' . $this->apiKey,
                'Accept: application/json',
            ],
        ]);

        $body = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($body === false) {
            throw new RuntimeException('cURL error: ' . $error);
        }

        if ($status !== 200) {
            throw new RuntimeException(
                sprintf('whois %s: HTTP %d: %s', $domain, $status, $body)
            );
        }

        $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
        return is_array($data) ? $data : [];
    }
}

$client = new WhoisClient(getenv('WHOISJSON_API_KEY') ?: '');
$data = $client->lookup('example.com');

echo ($data['name'] ?? '') . ' expires ' . ($data['expires'] ?? 'N/A') . PHP_EOL;
Remaining requests: every response includes aRemaining-Requests header. With cURL, enableCURLOPT_HEADER or use a header callback if you need to track monthly quota in workers.
Cache vs live data: default lookups may be served from the 3-hour cache (1 credit). PassforceRefresh: true only when you need live registry data — it costs 2 credits on Pro+. See the WHOIS caching guide.

Optional: Same Lookup with Guzzle

If your app already depends on Guzzle, the same contract maps cleanly — useful in Laravel HTTP clients and Symfony services.

WhoisGuzzle.phpPHP
<?php

use GuzzleHttp\Client;

$http = new Client([
    'base_uri' => 'https://whoisjson.com/api/v1/',
    'timeout'  => 15,
    'headers'  => [
        'Authorization' => 'TOKEN=' . getenv('WHOISJSON_API_KEY'),
        'Accept'        => 'application/json',
    ],
]);

$response = $http->get('whois', [
    'query' => ['domain' => 'example.com'],
]);

$data = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
echo $data['registrar']['name'] ?? 'N/A';

Safe Field Access

Unregistered domains and privacy-protected records omit fields your code might assume exist. Always null-coalesce nested keys.

summarize.phpPHP
<?php

function summarize(array $w): void
{
    $registrar = $w['registrar']['name'] ?? 'N/A';

    echo "Domain    : " . ($w['name'] ?? '') . PHP_EOL;
    echo "Registered: " . (($w['registered'] ?? false) ? 'yes' : 'no') . PHP_EOL;
    echo "Registrar : {$registrar}" . PHP_EOL;
    echo "Created   : " . ($w['created'] ?? 'N/A') . PHP_EOL;
    echo "Expires   : " . ($w['expires'] ?? 'N/A') . PHP_EOL;
    echo "Source    : " . ($w['source'] ?? 'N/A') . PHP_EOL;

    if (isset($w['age']) && is_array($w['age'])) {
        $days = $w['age']['days'] ?? 0;
        $isNew = !empty($w['age']['isNewlyRegistered']) ? 'yes' : 'no';
        echo "Age days  : {$days} (new={$isNew})" . PHP_EOL;
    }

    if (isset($w['expiration']) && is_array($w['expiration'])) {
        $left = $w['expiration']['daysLeft'] ?? 0;
        $soon = !empty($w['expiration']['isExpiringSoon']) ? 'yes' : 'no';
        echo "Days left : {$left} (soon={$soon})" . PHP_EOL;
    }
}

For every field in the payload, see the WHOIS JSON field reference. When contacts are missing, follow working with redacted WHOIS.

Handle HTTP 429 and Retries

Rate limits return429 Too Many Requests. Treat them as backpressure, not WHOIS data errors. Retry with exponential backoff and a hard attempt limit. Full patterns are documented in the rate limits guide.

retry.phpPHP
<?php

function isRetryable(Throwable $e): bool
{
    $msg = $e->getMessage();
    return str_contains($msg, 'HTTP 429')
        || str_contains($msg, 'HTTP 502')
        || str_contains($msg, 'HTTP 503')
        || str_contains($msg, 'HTTP 504');
}

function lookupWithRetry(WhoisClient $client, string $domain, int $maxAttempts = 4): array
{
    $lastError = null;

    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        try {
            return $client->lookup($domain);
        } catch (Throwable $e) {
            $lastError = $e;
            if (!isRetryable($e) || $attempt === $maxAttempts) {
                break;
            }
            sleep($attempt * $attempt);
        }
    }

    throw $lastError ?? new RuntimeException('WHOIS lookup failed');
}

Do not retry code 400 | or code 401 | — fix validation or authentication instead.

Bulk Lookups with curl_multi

For portfolios or threat feeds, bound concurrency. PHP’s code curl_multi | runs several transfers in parallel without blocking on each response sequentially.

bulk.phpPHP
<?php

/**
 * @param list<string> $domains
 * @return array<string, array{ok: bool, data?: array, error?: string}>
 */
function bulkLookup(string $apiKey, array $domains, int $concurrency = 4): array
{
    $results = [];
    $chunks = array_chunk($domains, max(1, $concurrency));

    foreach ($chunks as $chunk) {
        $mh = curl_multi_init();
        $handles = [];

        foreach ($chunk as $domain) {
            $url = 'https://whoisjson.com/api/v1/whois?' . http_build_query([
                'domain' => $domain,
            ]);
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 15,
                CURLOPT_HTTPHEADER => [
                    'Authorization: TOKEN=' . $apiKey,
                    'Accept: application/json',
                ],
            ]);
            curl_multi_add_handle($mh, $ch);
            $handles[$domain] = $ch;
        }

        do {
            $status = curl_multi_exec($mh, $active);
            if ($active) {
                curl_multi_select($mh);
            }
        } while ($active && $status === CURLM_OK);

        foreach ($handles as $domain => $ch) {
            $body = curl_multi_getcontent($ch);
            $http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);

            if ($http !== 200 || $body === false || $body === null) {
                $results[$domain] = [
                    'ok' => false,
                    'error' => sprintf('HTTP %d', $http),
                ];
                continue;
            }

            try {
                $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
                $results[$domain] = [
                    'ok' => true,
                    'data' => is_array($data) ? $data : [],
                ];
            } catch (Throwable $e) {
                $results[$domain] = [
                    'ok' => false,
                    'error' => $e->getMessage(),
                ];
            }
        }

        curl_multi_close($mh);
    }

    return $results;
}

Start with concurrency 3–5 on the free tier. Increase carefully on paid plans — see bulk WHOIS lookups for plan selection and pacing.

Use Case: Flag Newly Registered Domains

Use RDAPage.isNewlyRegistered when present. This mirrors the newly registered domains threat-signal guide.

nrd.phpPHP
<?php

function isNewlyRegistered(array $w): bool
{
    if (empty($w['registered'])) {
        return false;
    }

    return !empty($w['age']['isNewlyRegistered']);
}

$apiKey = getenv('WHOISJSON_API_KEY') ?: '';
$domains = ['brand-promo-deal.xyz', 'example.com', 'secure-login-update.top'];

foreach (bulkLookup($apiKey, $domains, 4) as $domain => $result) {
    if (!$result['ok']) {
        fwrite(STDERR, "{$domain} ERROR {$result['error']}" . PHP_EOL);
        continue;
    }

    $whois = $result['data'];
    if (isNewlyRegistered($whois)) {
        $days = $whois['age']['days'] ?? 0;
        echo "{$domain} NEWLY_REGISTERED age={$days}d" . PHP_EOL;
    }
}

Limits and Best Practices

  • Always setCURLOPT_TIMEOUT (or Guzzletimeout) — never hang a PHP-FPM worker on a slow registry path.
  • Store the API key in an environment variable or secret manager, not in source orwp-config.php commits.
  • Call WhoisJSON from the server side only — never expose the token in browser JavaScript.
  • Prefer the default cache for portfolio scans; use_forceRefresh=1 selectively (see caching guide).
  • Treat empty contacts as normal after GDPR — see redacted WHOIS guidance.
  • Prefer domain monitoring for continuous drift; use PHP clients for onboarding, cron jobs and on-demand enrichment.

Frequently Asked Questions

How do I query WHOIS data in PHP?

Send an authenticated GET to https://whoisjson.com/api/v1/whois?domain=example.com with Authorization: TOKEN=YOUR_API_KEY, then decode the JSON body with json_decode(..., true).

Do I need a PHP WHOIS Composer package?

No. ext-curl (or Guzzle) plus json_decode is enough. A REST API avoids maintaining registrar-specific text parsers in PHP.

How should PHP code handle HTTP 429 from a WHOIS API?

Retry with exponential backoff and a maximum attempt count. Do not treat 429 as “domain not found”. Read Remaining-Requests when available.

How can I look up many domains in parallel in PHP?

Use curl_multi with a fixed concurrency chunk size. Bound parallelism to stay within your plan rate limit.

Does this work with Laravel?

Yes. Use Guzzle via Http::withHeaders([...])->get(...), or inject the cURL client as a service. Keep the API key in .env as WHOISJSON_API_KEY.

Is there a free tier for PHP integrations?

Yes. WhoisJSON includes 1,000 free requests per month across WHOIS, DNS, SSL, availability and related endpoints — no credit card required.

Conclusion

PHP and a REST WHOIS API are a practical combination for domain checks in Laravel, WordPress backends and custom SaaS: predictable JSON, explicit timeouts, and controlled concurrency with curl_multi. Start with a small client class, null-safe field access, retry 429s carefully, then scale bulk jobs in chunks.

The WhoisJSON API handles TLD routing, RDAP fallback, and structured JSON — so your PHP code stays focused on application logic rather than registry parsers.

Start querying WHOIS data in PHP

Create a free account and get 1,000 API requests per month. No credit card required.

Ready to build?

WHOIS, DNS, SSL, subdomain discovery and domain monitoring — one API token, 1,000 free requests every month.