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.
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
availablefield 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 https://whoisjson.com/api/v1/domain-availability?domain=example.com
Authorization: TOKEN=YOUR_API_KEY
Response:
{
"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.
curl -s \
"https://whoisjson.com/api/v1/domain-availability?domain=yourdomain.com" \
-H "Authorization: TOKEN=YOUR_API_KEY"
Sample output:
{
"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.
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.
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.
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
| Plan | Monthly quota | Rate limit | Delay between requests |
|---|---|---|---|
| Basic (Free) | 1,000 req | 20 req/min | ~3 s |
| Pro ($10/mo) | 30,000 req | 40 req/min | ~1.5 s |
| Ultra ($30/mo) | 150,000 req | 60 req/min | ~1 s |
| Mega ($80+/mo) | Unlimited | 80–900 req/min | ~0.1 s |
Python — bulk check with retry on 429
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
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.
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.