Engineering

How to Check Domain Availability Programmatically (API Guide with Code Examples)

April 5, 202611 min readDomain Availability · REST API · Python · Node.js

Introduction

Checking whether a domain name is available is one of the most common operations in domain tooling — but doing it reliably at scale is harder than it looks. If you are building a domain registrar, a brand monitoring dashboard, a SaaS platform that provisions subdomains, a marketplace where users claim handles, or a drop-catching system, you cannot afford to open a browser and type into a search box for every domain you need to evaluate.

The right approach is an API call: deterministic input, structured JSON output, HTTP status codes you can branch on, and a single endpoint that handles the protocol complexity for you across 1,500+ TLDs. This guide walks through why the naive programmatic approaches fail, how a domain availability API works, and gives you production-ready code in cURL, Python, and Node.js — including bulk checking with proper rate-limit handling.

The Naive Approach and Why It Fails

The most common DIY attempts all share the same failure modes. Here is what developers typically reach for and where each breaks down.

Raw TCP WHOIS (port 43)

The original WHOIS protocol operates over TCP on port 43. You open a socket, send a domain name followed by \r\n, and read plain text back. It works for a quick test, but it is not production-grade:

  • No standard schema. Every registry returns a different text format. Verisign ( .com, .net) uses one layout, Nominet ( .uk) uses another, and most ccTLDs are entirely idiosyncratic. You end up maintaining a parser per TLD family.
  • Aggressive rate limiting. Verisign caps anonymous .com/.net queries at a few hundred per day per IP. Most ccTLD registries apply even stricter throttles and will silently block an IP that exceeds the limit, returning nothing rather than an error.
  • No availability signal — only registration data. A WHOIS query tells you a domain is registered. The absence of a WHOIS record is an indirect signal that the domain may be available — but it is not authoritative. The registry is the only authoritative source for availability status.
  • Blocked outbound port 43. Firewalls in corporate environments and many cloud providers block outbound TCP/43 by default.

whois CLI / python-whois / node-whois

Library wrappers around raw WHOIS inherit every limitation above and add their own: they depend on locally installed whois binaries (not available on serverless runtimes), they parse text with regular expressions that break silently when registries update their output format, and they provide no built-in rate limiting or retry logic. The python-whois library, for example, parses a curated list of ~200 TLD patterns — which covers the most common cases but fails on new gTLDs and many ccTLDs.

Checking DNS resolution as a proxy

Some developers check whether a domain resolves in DNS and infer availability from the absence of an A record. This approach is unreliable in both directions: unregistered domains can resolve because of wildcard DNS or parking records set by the registry itself, and registered domains can have no DNS records at all (not every registrant sets up a website). DNS resolution is not a substitute for a registry availability check.

The fundamental problem: availability is a registry-level concept. Neither WHOIS nor DNS was designed to answer the question "can I register this name right now?" — that answer comes from the registry's EPP (Extensible Provisioning Protocol) system. A domain availability API proxies an EPP-based availability check and returns a structured response you can actually branch on.

Using a Domain Availability API

A REST domain availability API wraps the registry availability check in a simple HTTPS endpoint. You get structured JSON, consistent behavior across TLDs, a single authentication mechanism, and no socket management or text parsing on your side.

What it brings to the table

  • Structured JSON output. A boolean available field you can branch on directly — no string parsing, no regex, no format variations.
  • 1,500+ TLD coverage. The API routes each query to the correct registry and handles WHOIS/RDAP protocol negotiation automatically. You send one request format regardless of whether the TLD is .com, .io, .photography, or .рф.
  • No port 43 dependency. All requests are standard HTTPS to a single host — no firewall issues, no binary dependencies, works on any runtime including serverless.
  • Rate limiting handled for you. The API abstracts registry-level rate limits through IP pooling and caching layers. You get predictable throughput based on your plan, not the registry's whim.

The WhoisJSON endpoint

The /domain-availability endpoint accepts a single domain query parameter and returns a two-field JSON object:

GET /api/v1/domain-availabilityHTTP
GET https://whoisjson.com/api/v1/domain-availability?domain=example.com
Authorization: TOKEN=YOUR_API_KEY

Response:

200 OKJSON
{
  "domain": "example.com",
  "available": false
}

The available field is true when the domain can be registered right now, false when it is already registered or otherwise reserved. The base URL is https://whoisjson.com/api/v1 and authentication uses the Authorization: TOKEN=<YOUR_API_KEY> header. Your first 1,000 requests are free — no credit card required.

Quick Start — cURL Example

The fastest way to verify your API key works and see the response format — no code, no setup, 30 seconds.

TerminalcURL
curl -s \
  "https://whoisjson.com/api/v1/domain-availability?domain=yourdomain.com" \
  -H "Authorization: TOKEN=YOUR_API_KEY"

Sample output:

ResponseJSON
{
  "domain": "yourdomain.com",
  "available": true
}

Replace code YOUR_API_KEY | with the key from your dashboard. The code -s | flag suppresses the progress meter. Pipe to code | python3 -m json.tool | if you want pretty-printed output in the terminal.

Python Example

The following script uses requests to query availability for a list of domains. It handles three outcome states: available, taken, and error (non-200 response), and prints a clean summary at the end.

check_availability.pyPython
import time
import requests

API_KEY  = "YOUR_API_KEY"
BASE_URL = "https://whoisjson.com/api/v1"
HEADERS  = {"Authorization": f"TOKEN={API_KEY}"}

def check_availability(domain: str) -> dict:
    """
    Returns a dict with keys: domain, status ("available" | "taken" | "error"),
    and optionally "detail" for error cases.
    """
    try:
        resp = requests.get(
            f"{BASE_URL}/domain-availability",
            params={"domain": domain},
            headers=HEADERS,
            timeout=10,
        )
        if resp.status_code == 200:
            data = resp.json()
            status = "available" if data.get("available") else "taken"
            return {"domain": domain, "status": status}
        elif resp.status_code == 401:
            return {"domain": domain, "status": "error", "detail": "Invalid API key"}
        elif resp.status_code == 429:
            return {"domain": domain, "status": "error", "detail": "Rate limit exceeded"}
        else:
            return {"domain": domain, "status": "error", "detail": f"HTTP {resp.status_code}"}
    except requests.exceptions.Timeout:
        return {"domain": domain, "status": "error", "detail": "Request timed out"}
    except requests.exceptions.RequestException as exc:
        return {"domain": domain, "status": "error", "detail": str(exc)}


if __name__ == "__main__":
    domains = [
        "mybrand2026.com",
        "mybrand2026.io",
        "mybrand2026.net",
        "mybrand2026.co",
        "mybrand2026.app",
    ]

    results = []
    for domain in domains:
        result = check_availability(domain)
        results.append(result)
        icon = "✓" if result["status"] == "available" else ("✗" if result["status"] == "taken" else "!")
        detail = f"  [{result['detail']}]" if result.get("detail") else ""
        print(f"  {icon}  {domain:<30} {result['status']}{detail}")
        time.sleep(0.5)  # stay within rate limits

    available = [r["domain"] for r in results if r["status"] == "available"]
    print(f"\n{len(available)} of {len(domains)} domain(s) available: {', '.join(available) or 'none'}")

Install the dependency with code pip install requests | if needed. The 500 ms delay between requests is intentional — see the bulk checking section for a discussion of rate limits. For fewer than 10 domains you can remove it safely.

Node.js Example

The same logic in Node.js using native fetch (available since Node 18) and async/await. No external dependencies required.

checkAvailability.mjsNode.js
const API_KEY  = "YOUR_API_KEY";
const BASE_URL = "https://whoisjson.com/api/v1";

async function checkAvailability(domain) {
  const url = `${BASE_URL}/domain-availability?domain=${encodeURIComponent(domain)}`;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 10_000);

  try {
    const res = await fetch(url, {
      headers: { Authorization: `TOKEN=${API_KEY}` },
      signal: controller.signal,
    });

    clearTimeout(timer);

    if (res.status === 200) {
      const data = await res.json();
      return { domain, status: data.available ? "available" : "taken" };
    }
    if (res.status === 401) return { domain, status: "error", detail: "Invalid API key" };
    if (res.status === 429) return { domain, status: "error", detail: "Rate limit exceeded" };
    return { domain, status: "error", detail: `HTTP ${res.status}` };

  } catch (err) {
    clearTimeout(timer);
    const detail = err.name === "AbortError" ? "Request timed out" : err.message;
    return { domain, status: "error", detail };
  }
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

const domains = [
  "mybrand2026.com",
  "mybrand2026.io",
  "mybrand2026.net",
  "mybrand2026.co",
  "mybrand2026.app",
];

(async () => {
  const results = [];

  for (const domain of domains) {
    const result = await checkAvailability(domain);
    results.push(result);

    const icon = result.status === "available" ? "✓" : result.status === "taken" ? "✗" : "!";
    const detail = result.detail ? `  [${result.detail}]` : "";
    console.log(`  ${icon}  ${domain.padEnd(30)} ${result.status}${detail}`);

    await sleep(500);
  }

  const available = results.filter((r) => r.status === "available").map((r) => r.domain);
  console.log(`\n${available.length} of ${domains.length} domain(s) available: ${available.join(", ") || "none"}`);
})();

Run with node checkAvailability.mjs (Node 18+). The AbortController pattern provides a 10-second hard timeout per request — important when querying obscure TLDs whose registries can be slow to respond.

Handling Edge Cases

Domain availability is not always a clean true/false. Here are the scenarios that will trip up naive implementations.

Exotic TLDs and reserved names

Some TLDs restrict registrations to specific entities (country nationals, brand owners, professional bodies). A name that is technically "unregistered" in the registry database may still be unregisterable by you — for example, code .bank | domains require FDIC membership, and code .amazon | is a brand TLD closed to public registration. The availability API tells you whether the name is registered; eligibility requirements are a separate concern you must verify with the registrar.

Names in pendingDelete

A domain in pendingDelete EPP status is technically still registered at the registry — it has not been released yet. An availability check on such a name will return "available": false even though it will become available within 5 days. If you are drop-catching, combine the availability endpoint with a WHOIS expiry status check to identify names in the pending delete window.

Rate limit errors (HTTP 429)

If you exceed your plan's rate limit, the API returns 429 Too Many Requests. The correct response is to back off and retry — not to discard the result. Both code examples above detect 429 and surface it as an error state so you can re-queue the domain. For sustained bulk workloads, use the delay strategy in the next section.

Network timeouts on slow registries

Some ccTLD registries have slow WHOIS/RDAP servers that occasionally spike to 5–8 seconds per query. Always set an explicit timeout on your HTTP client (10 seconds is a safe ceiling) and treat timeouts as retryable errors rather than definitive answers. Never interpret a timeout as "domain is available."

IDN (Internationalized Domain Names)

For domains with non-ASCII characters ( münchen.de, 日本語.jp), pass the Punycode-encoded form to the API ( m%C3%BCnchen.de or the xn-- ACE label). In Python, convert with domain.encode("idna").decode("ascii") before passing to the request. In Node.js, use the built-in url.domainToASCII() function.

Always validate the domain format first. Send malformed strings (missing TLD, double dots, spaces) and you will get 400 Bad Request. A simple regex check before calling the API — e.g. /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i — eliminates noise in your error logs.

Bulk Domain Availability Checking

Checking availability across a large list — 50 TLD variants of a brand name, a portfolio of drop candidates, or a set of user-requested handles — requires a loop with rate-limit awareness.

Rate limits by plan

PlanMonthly quotaRate limitDelay between requests
Basic (Free)1,000 req20 req/min~3 s
Pro ($10/mo)30,000 req40 req/min~1.5 s
Ultra ($30/mo)150,000 req60 req/min~1 s
Mega ($80+/mo)Unlimited80–900 req/min~0.1 s

Python — bulk check with retry on 429

bulk_check.pyPython
import time
import requests

API_KEY      = "YOUR_API_KEY"
BASE_URL     = "https://whoisjson.com/api/v1"
HEADERS      = {"Authorization": f"TOKEN={API_KEY}"}
DELAY        = 1.0   # seconds between requests (adjust per plan)
MAX_RETRIES  = 3


def check_with_retry(domain: str) -> dict:
    for attempt in range(1, MAX_RETRIES + 1):
        try:
            resp = requests.get(
                f"{BASE_URL}/domain-availability",
                params={"domain": domain},
                headers=HEADERS,
                timeout=10,
            )
            if resp.status_code == 200:
                data = resp.json()
                return {"domain": domain, "available": data.get("available")}
            elif resp.status_code == 429:
                wait = 60 * attempt  # back off 60 s, 120 s, 180 s
                print(f"  Rate limited on {domain}, waiting {wait}s (attempt {attempt}/{MAX_RETRIES})")
                time.sleep(wait)
            else:
                return {"domain": domain, "available": None, "error": f"HTTP {resp.status_code}"}
        except requests.exceptions.RequestException as exc:
            if attempt == MAX_RETRIES:
                return {"domain": domain, "available": None, "error": str(exc)}
            time.sleep(5 * attempt)
    return {"domain": domain, "available": None, "error": "Max retries exceeded"}


brand  = "mybrand2026"
tlds   = ["com", "net", "org", "io", "co", "app", "dev", "ai", "xyz", "online"]
domains = [f"{brand}.{tld}" for tld in tlds]

results = []
for domain in domains:
    result = check_with_retry(domain)
    results.append(result)
    status = "available" if result["available"] else ("taken" if result["available"] is False else "error")
    print(f"  {domain:<30} {status}")
    time.sleep(DELAY)

print("\n--- Available ---")
for r in results:
    if r["available"]:
        print(f"  {r['domain']}")

Node.js — bulk check with rate limit backoff

bulkCheck.mjsNode.js
const API_KEY    = "YOUR_API_KEY";
const BASE_URL   = "https://whoisjson.com/api/v1";
const DELAY_MS   = 1000;   // ms between requests (adjust per plan)
const MAX_RETRY  = 3;

async function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function checkWithRetry(domain, attempt = 1) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 10_000);

  try {
    const res = await fetch(
      `${BASE_URL}/domain-availability?domain=${encodeURIComponent(domain)}`,
      { headers: { Authorization: `TOKEN=${API_KEY}` }, signal: controller.signal }
    );
    clearTimeout(timer);

    if (res.status === 200) {
      const data = await res.json();
      return { domain, available: data.available };
    }
    if (res.status === 429 && attempt <= MAX_RETRY) {
      const wait = 60_000 * attempt;
      console.log(`  Rate limited on ${domain}, waiting ${wait / 1000}s (attempt ${attempt})`);
      await sleep(wait);
      return checkWithRetry(domain, attempt + 1);
    }
    return { domain, available: null, error: `HTTP ${res.status}` };

  } catch (err) {
    clearTimeout(timer);
    if (attempt <= MAX_RETRY) {
      await sleep(5000 * attempt);
      return checkWithRetry(domain, attempt + 1);
    }
    return { domain, available: null, error: err.message };
  }
}

const brand   = "mybrand2026";
const tlds    = ["com", "net", "org", "io", "co", "app", "dev", "ai", "xyz", "online"];
const domains = tlds.map((tld) => `${brand}.${tld}`);

(async () => {
  const results = [];

  for (const domain of domains) {
    const result = await checkWithRetry(domain);
    results.push(result);
    const status = result.available === true ? "available" : result.available === false ? "taken" : "error";
    console.log(`  ${domain.padEnd(30)} ${status}`);
    await sleep(DELAY_MS);
  }

  console.log("\n--- Available ---");
  results.filter((r) => r.available).forEach((r) => console.log(`  ${r.domain}`));
})();

Combining Availability with WHOIS Data

The availability endpoint answers one question: can I register this name right now? For richer workflows, you will often want to combine it with full WHOIS data.

Use case: brand monitoring

You are tracking a set of brand-adjacent domains — typosquats, competitor names, new TLD variants. For each domain, you want to know: is it registered, and if so, who registered it and when? The workflow is: check availability first (one cheap call), and only if the domain is taken, fetch full WHOIS data for registrar, registrant, creation date, and status.

brand_monitor.pyPython
import time
import requests

API_KEY  = "YOUR_API_KEY"
BASE_URL = "https://whoisjson.com/api/v1"
HEADERS  = {"Authorization": f"TOKEN={API_KEY}"}


def check_availability(domain: str) -> bool | None:
    resp = requests.get(f"{BASE_URL}/domain-availability", params={"domain": domain}, headers=HEADERS, timeout=10)
    if resp.status_code == 200:
        return resp.json().get("available")
    return None


def get_whois(domain: str) -> dict:
    resp = requests.get(f"{BASE_URL}/whois", params={"domain": domain}, headers=HEADERS, timeout=10)
    resp.raise_for_status()
    return resp.json()


domains = ["mybrand-typo.com", "mybrannd.com", "mybramd.com"]

for domain in domains:
    available = check_availability(domain)
    if available is False:
        data = get_whois(domain)
        registrar = (data.get("registrar") or {}).get("name", "unknown")
        created   = data.get("created", "unknown")
        expires   = data.get("expires", "unknown")
        status    = ", ".join(data.get("status") or [])
        print(f"[TAKEN] {domain}")
        print(f"        Registrar : {registrar}")
        print(f"        Created   : {created}")
        print(f"        Expires   : {expires}")
        print(f"        EPP status: {status}")
    elif available is True:
        print(f"[FREE]  {domain}")
    else:
        print(f"[ERR]   {domain}")
    time.sleep(0.6)

For a deeper dive into interpreting the WHOIS response fields — statusAnalysis, nsAnalysis, age, expiration — see the WHOIS API JSON Response Field Reference and the complete guides for Python and Node.js.

FAQ

How accurate is a domain availability API?

A well-implemented API routes each query to the registry's EPP availability check, which is the authoritative source. The answer is accurate at the moment of the call. Because domains are registered and released continuously, a domain that returns code "available": true | can be registered by someone else in the seconds before you act on that response. For time-sensitive workflows (drop-catching, real-time checkout), availability checks should be as close to the registration step as possible and never cached for more than a few seconds.

Can I check availability for any TLD?

The WhoisJSON API covers 1,500+ TLDs including all major gTLDs, new gTLDs, and most ccTLDs. A small number of ccTLD registries operate closed WHOIS/RDAP servers that do not support availability queries. For those, the API falls back to a WHOIS-based inference approach — checking for the absence of a registration record. Coverage is documented in the a(href="/documentation") API documentation | .

What is the difference between domain availability and WHOIS lookup?

A domain availability check answers: "can I register this name today?" A WHOIS lookup answers: "what is the public registration record for this name?" Availability is a real-time registry state query. WHOIS returns historical and current registration data (registrar, registrant contacts, dates, nameservers, EPP status). They serve different purposes — use availability for checkout flows and domain searches, use WHOIS for intelligence gathering, monitoring, and compliance.

How many domains can I check per minute on the free plan?

The Basic (free) plan allows 20 requests per minute across all endpoints combined. For availability checks alone that means up to 20 domains per minute with a 3-second inter-request delay. The monthly quota on the Basic plan is 1,000 requests — no credit card required to activate.

Start checking domain availability in minutes

Register for a free API key and make your first availability call in under 30 seconds. 1,000 requests included — no credit card.

Domain Availability API

Check Any Domain in One API Call

Structured JSON, 1,500+ TLDs, and the same endpoint whether you are checking .com or .photography. Start with 1,000 free requests.

1,500+ TLDs coveredJSON response1,000 req free, no card