Introduction
WHOIS data is useful across a surprising range of Python projects. Security engineers feed it into threat-intelligence pipelines to catch newly-registered phishing domains before they send their first email. SaaS platforms verify domain ownership at onboarding. Compliance scripts check registrar and expiry data on a schedule. In all these scenarios, you need structured, reliable domain data — not a raw text dump.
The two most common Python approaches — the python-whois library and a subprocess call to the system whois binary — both inherit the fragility of the legacy WHOIS protocol: inconsistent field names across registrars, missing data on privacy-protected domains, no structured error codes, and hundreds of parsing edge cases you have to maintain yourself. A REST API eliminates all of that. You send a GET request, you get back consistent JSON. This guide shows you how to integrate the WhoisJSON WHOIS API into Python — from a single lookup to a production-grade async bulk scanner.
Prerequisites
- Python 3.8+ — all examples use standard syntax compatible with 3.8 and above.
- requests library — install with
pip install requests. - A free WhoisJSON API key — sign up here for 1,000 free requests per month. No credit card required.
- For the async section:
aiohttp— install withpip install aiohttp.
Basic WHOIS Lookup in Python
The simplest integration is a single requests.get call. Your API key goes in the Authorization header as Token=YOUR_API_KEY. The base URL for all WhoisJSON endpoints is https://whoisjson.com/api/v1.
import requests
API_KEY = "YOUR_API_KEY" # from your dashboard
BASE_URL = "https://whoisjson.com/api/v1"
def whois_lookup(domain: str) -> dict:
response = requests.get(
f"{BASE_URL}/whois",
params={"domain": domain},
headers={"Authorization": f"Token={API_KEY}"},
timeout=10,
)
response.raise_for_status()
return response.json()
data = whois_lookup("example.com")
print(data)The API returns a normalized JSON object. Here is a representative excerpt — notice the pre-computed helper objects that save you from manual date arithmetic:
{
"name": "example.com",
"registered": true,
"source": "rdap",
"created": "1995-08-14",
"changed": "2024-08-13",
"expires": "2026-08-13",
"age": {
"days": 10743,
"months": 354,
"years": 29,
"isNewlyRegistered": false,
"isYoung": false
},
"expiration": {
"daysLeft": 152,
"isExpiringSoon": false,
"isExpired": false
},
"registrar": {
"name": "IANA",
"url": "https://www.iana.org"
},
"nameserver": ["a.iana-servers.net", "b.iana-servers.net"]
} Remaining-Requests header (e.g. Remaining-Requests: 952). Access it with response.headers.get("Remaining-Requests") to monitor your quota programmatically.Parsing the Response
Use .get() for safe key access — some fields are absent when a domain is not registered or when WHOIS data is redacted for privacy. The source field tells you whether the response came from RDAP (richer data, structured contacts) or legacy WHOIS.
data = whois_lookup("github.com")
registrar = data.get("registrar", {}).get("name", "N/A")
created = data.get("created", "N/A")
expires = data.get("expires", "N/A")
nameservers = data.get("nameserver", [])
source = data.get("source", "whois")
# Pre-computed expiration helpers (rdap only)
expiration = data.get("expiration") or {}
days_left = expiration.get("daysLeft")
is_expiring = expiration.get("isExpiringSoon", False)
print(f"Registrar : {registrar}")
print(f"Created : {created}")
print(f"Expires : {expires}")
print(f"Days left : {days_left}")
print(f"Expiring : {is_expiring}")
print(f"NS : {', '.join(nameservers)}")
print(f"Source : {source}") source is rdap, the response includes extra enriched fields — age, expiration, statusAnalysis, and nsAnalysis — that are not present in legacy WHOIS responses.Bulk Domain Lookup
For sequential batch processing, loop over your list and insert a short delay between requests. A 300 ms pause keeps you well inside the rate limit while processing hundreds of domains without issues.
import time
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://whoisjson.com/api/v1"
DOMAINS = [
"github.com",
"stripe.com",
"notion.so",
"linear.app",
"vercel.com",
]
def whois_lookup(session: requests.Session, domain: str) -> dict | None:
try:
r = session.get(
f"{BASE_URL}/whois",
params={"domain": domain},
timeout=10,
)
r.raise_for_status()
return r.json()
except requests.RequestException as exc:
print(f"[WARN] {domain}: {exc}")
return None
results = {}
with requests.Session() as session:
session.headers["Authorization"] = f"Token={API_KEY}"
for domain in DOMAINS:
results[domain] = whois_lookup(session, domain)
time.sleep(0.3) # respect the rate limit
for domain, data in results.items():
if data:
print(f"{domain} — expires {data.get('expires', 'N/A')}")Using a requests.Session reuses the underlying TCP connection across requests, reducing latency and avoiding TLS handshake overhead on every call.
Async Bulk Lookup with aiohttp
For high-throughput pipelines — thousands of domains or time-sensitive enrichment tasks — switch to an async model with aiohttp and asyncio.gather. A Semaphore caps concurrent in-flight requests to stay within the API rate limit.
import asyncio
import aiohttp
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://whoisjson.com/api/v1"
MAX_CONCURRENT = 5 # max parallel requests
DOMAINS = [
"github.com",
"stripe.com",
"notion.so",
"linear.app",
"vercel.com",
"figma.com",
"discord.com",
]
async def fetch_whois(
session: aiohttp.ClientSession,
semaphore: asyncio.Semaphore,
domain: str,
) -> tuple[str, dict | None]:
async with semaphore:
try:
async with session.get(
f"{BASE_URL}/whois",
params={"domain": domain},
timeout=aiohttp.ClientTimeout(total=10),
) as response:
response.raise_for_status()
return domain, await response.json()
except Exception as exc:
print(f"[WARN] {domain}: {exc}")
return domain, None
async def bulk_whois(domains: list[str]) -> dict:
semaphore = asyncio.Semaphore(MAX_CONCURRENT)
headers = {"Authorization": f"Token={API_KEY}"}
async with aiohttp.ClientSession(headers=headers) as session:
tasks = [
fetch_whois(session, semaphore, domain)
for domain in domains
]
pairs = await asyncio.gather(*tasks)
return dict(pairs)
results = asyncio.run(bulk_whois(DOMAINS))
for domain, data in results.items():
if data:
print(f"{domain} — registrar: {data.get('registrar', {}).get('name', 'N/A')}") MAX_CONCURRENT at 5 or below on free plans. On paid plans with higher rate limits you can safely raise it to 10–20 depending on your subscription tier.Error Handling & Edge Cases
The WhoisJSON API uses standard HTTP status codes. The most common ones you will encounter in production:
| Status | Meaning | What to do |
|---|---|---|
200 | Success | Parse the JSON body normally. |
400 | Bad request | Check that the domain parameter is a valid FQDN. |
401 | Invalid token | Verify your API key and the Token= prefix in the header. |
403 | Email not validated | Confirm your account email then retry. |
429 | Rate limit exceeded | Back off and retry after a delay. Upgrade your plan for higher limits. |
500 | Internal error | Retry once; if persistent, check the status page. |
Here is a robust wrapper with retry logic for transient errors and graceful handling for unregistered domains:
import time
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://whoisjson.com/api/v1"
def whois_lookup(domain: str, retries: int = 2) -> dict | None:
headers = {"Authorization": f"Token={API_KEY}"}
for attempt in range(retries + 1):
try:
r = requests.get(
f"{BASE_URL}/whois",
params={"domain": domain},
headers=headers,
timeout=10,
)
if r.status_code == 429:
# Rate limited — wait and retry
wait = 2 ** attempt
print(f"[429] Rate limited. Retrying in {wait}s...")
time.sleep(wait)
continue
r.raise_for_status()
data = r.json()
# Domain exists but is not registered
if not data.get("registered", True):
print(f"[INFO] {domain} is not registered.")
return None
return data
except requests.Timeout:
print(f"[WARN] {domain}: request timed out (attempt {attempt + 1})")
except requests.HTTPError as exc:
# 4xx errors are not retryable
print(f"[ERROR] {domain}: HTTP {exc.response.status_code}")
return None
except requests.RequestException as exc:
print(f"[ERROR] {domain}: {exc}")
return None
return None
Real-World Use Case: Flag Newly Registered Domains
Domains registered within the last 30 days are a well-known high-risk signal: phishing kits, malware C2 infrastructure, and brand-impersonation campaigns almost always run on freshly registered domains. The WhoisJSON API computes age.isNewlyRegistered for you — no date parsing required.
The following script takes a list of suspected domains, looks them up, and outputs a report of the ones that are newly registered:
import time
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://whoisjson.com/api/v1"
# Domains extracted from logs, threat feeds, or user reports
SUSPECTS = [
"paypa1-secure-login.com",
"netflix-account-verify.net",
"apple-id-support-team.org",
"github.com", # control — known-old domain
"stripe.com", # control — known-old domain
]
def check_domain(session: requests.Session, domain: str) -> dict | None:
try:
r = session.get(
f"{BASE_URL}/whois",
params={"domain": domain},
timeout=10,
)
r.raise_for_status()
return r.json()
except requests.RequestException as exc:
print(f"[WARN] {domain}: {exc}")
return None
flagged = []
with requests.Session() as session:
session.headers["Authorization"] = f"Token={API_KEY}"
for domain in SUSPECTS:
data = check_domain(session, domain)
if not data:
time.sleep(0.3)
continue
age = data.get("age") or {}
is_new = age.get("isNewlyRegistered", False)
is_young = age.get("isYoung", False)
days_old = age.get("days")
registrar = data.get("registrar", {}).get("name", "N/A")
registered = data.get("registered", False)
source = data.get("source", "whois")
if is_new:
risk = "HIGH"
elif is_young:
risk = "MEDIUM"
else:
risk = "LOW"
entry = {
"domain": domain,
"risk": risk,
"days_old": days_old,
"registrar": registrar,
"source": source,
}
if risk in ("HIGH", "MEDIUM"):
flagged.append(entry)
print(
f"[{risk:6}] {domain:<40} "
f"age={days_old}d registrar={registrar} source={source}"
)
time.sleep(0.3)
print(f"\n{'='*60}")
print(f"Flagged domains: {len(flagged)}")
for d in flagged:
print(f" - {d['domain']} ({d['days_old']}d old — {d['risk']} risk)") age and isNewlyRegistered fields are only populated when source is rdap. For legacy WHOIS responses, fall back to parsing the created string manually or treat the absence of age as a signal worth investigating on its own.Conclusion
You now have a complete Python toolkit for WHOIS lookups: a single-domain function with requests, a sequential bulk scanner with rate-limit-safe pacing, a high-throughput async version with aiohttp, production-grade error handling, and a real-world fraud detection script that flags newly registered domains in seconds.
The WhoisJSON API handles TLD routing, RDAP fallback, GDPR redaction normalization, and structured error codes — so your Python code stays clean and focused on your actual use case rather than parser maintenance.
Start querying WHOIS data now
Create a free account and get 1,000 API requests per month. No credit card required.