Why not just parse WHOIS yourself
I tried that first. Raw WHOIS is unstructured, every registry formats it differently, and GDPR turned half the fields into REDACTED FOR PRIVACY. Some registries throttle port 43 after a few dozen queries. A few ccTLDs barely answer at all.
RDAP was supposed to fix this with clean JSON, and it mostly does — but coverage is still patchy and the schema wobbles between registries.
What I actually needed was boring and specific: an expiry date that parses the same way every time, across the 23 TLDs my projects and clients use. I also do light OSINT work, so SSL cert data, subdomain takeover checks, and some kind of threat signal were on the wishlist.
The setup: same 150 domains, same machine, 3 timed runs each, medians recorded. Mostly I cared about one thing — does the expiry field come back populated?
The 12, side by side
| # | API | Free tier | Median latency | Expiry field | Notable extra |
|---|---|---|---|---|---|
| 1 | Raw RDAP (rdap.org) | Unlimited, no key | 240ms | Yes, when registry has RDAP | None |
| 2 | python-whois (GitHub) | Free library | Varies wildly | Empty on 6 of 23 TLDs | None |
| 3 | WhoisXML API | ~500 credits/mo | 610ms | Yes | History, brand monitoring |
| 4 | DomainTools Iris | Trial only | 700ms | Yes | Everything, enterprise price |
| 5 | WhoisAPI.com | ~100/mo | 520ms | Yes | Bulk endpoints |
| 6 | JsonWHOIS | ~100/mo | 480ms | Yes | Social data |
| 7 | IP2WHOIS | Free tier | 550ms | Yes | Simple, cheap |
| 8 | APILayer whois | ~100/mo | 590ms | Yes | Shared key with other APILayer APIs |
| 9 | Whoxy | Tiny free tier | 640ms | Yes | Reverse WHOIS |
| 10 | WhoisJson | Free tier | 510ms | Yes | DNS records |
| 11 | SecurityTrails | ~50/mo | 430ms | Partial | DNS history, OSINT gold |
| 12 | Domain WHOIS API (RapidAPI) | Free tier | 390ms | Yes | SSL, takeover check, threat score |
Latency numbers are from my box in Frankfurt; yours will differ. For a nightly cron the gap between 390ms and 610ms means nothing , it only matters if you're building something interactive.
Where things broke
Every landing page looks great. The failures only show up at 1 AM.
python-whois was the DIY route, and I wanted it to win. Free, no key, no vendor. It returned an empty expiry field for 6 of my 23 TLDs, including .ai and .dev, because the parser regexes didn't match what those registries send back. Fixing that means maintaining your own regex fork, and I've done that before. I'm not doing it again.
Raw RDAP is genuinely good for .com and .net. The bootstrap list had no RDAP server for two ccTLDs in my set, though, which dumped me right back to parsing port 43 text. One registry also rate-limited me around 30 requests per minute with zero warning in the docs.
One mid-tier API (I'll be polite and skip the name) returned a 2023 expiry date for a domain I'd renewed that January. Stale cache. A monitor built on that pages you about ghosts , or stays quiet while a real expiry creeps up, which is worse. I now cross-check every new provider against 5 freshly renewed domains before trusting it.
DomainTools is excellent, and priced accordingly. "Contact sales" is fine if you have a SOC. I don't.
And a self-own: I burned one provider's entire 100-request free tier in a single afternoon because I forgot to cache responses while debugging the test runner. Rookie mistake, happily admitted.
What ended up mattering
It came down to three things:
- Expiry field populated across weird TLDs. Half the field failed here.
- A free tier big enough to run a real monitor. 100 requests a month is a demo, not a tier.
- Extras that kill other tools. I was running separate scripts for SSL expiry and takeover checks. One API bundled all of it under a single key: the Domain WHOIS API on RapidAPI. WHOIS lookup, SSL cert info, subdomain takeover detection, threat score.
The takeover check earned its keep during the audit that started all this. It flagged an old blog subdomain CNAME'd to a Heroku app nobody had owned in two years:
{
"domain": "old-blog.example.com",
"cname": "old-blog.herokuapp.com",
"provider": "Heroku",
"vulnerable": true,
"detail": "CNAME resolves but no app claims the hostname"
}
The slug was unclaimed. Grab it, and you're serving content on a subdomain we controlled. Classic dangling DNS. The threat score held up too: it rated a lookalike domain I'd registered for phishing-simulation practice at 87/100, while my real domains sat around 10-15. Directionally right, which is all I ask from a heuristic.
There's also a GitHub repo if you want to read the code or file an issue.
How to use it
- Create a free RapidAPI account.
- Open the Domain WHOIS API listing and subscribe to the free tier.
- Copy your
x-rapidapi-keyfrom the dashboard. - Run it.
WHOIS lookup with curl:
curl -s "https://domain-whois-api2.p.rapidapi.com/whois?domain=example.com" \
-H "x-rapidapi-host: domain-whois-api2.p.rapidapi.com" \
-H "x-rapidapi-key: YOUR_KEY" | jq .
Subdomain takeover check:
curl -s "https://domain-whois-api2.p.rapidapi.com/subdomain-takeover?domain=example.com" \
-H "x-rapidapi-host: domain-whois-api2.p.rapidapi.com" \
-H "x-rapidapi-key: YOUR_KEY" | jq .
Python, hitting all four endpoints in one pass:
import requests
HOST = "domain-whois-api2.p.rapidapi.com"
HEADERS = {
"x-rapidapi-host": HOST,
"x-rapidapi-key": "YOUR_KEY",
}
def full_check(domain):
report = {}
for endpoint in ["whois", "ssl", "subdomain-takeover", "threat-score"]:
r = requests.get(
f"https://{HOST}/{endpoint}",
headers=HEADERS,
params={"domain": domain},
timeout=10,
)
r.raise_for_status()
report[endpoint] = r.json()
return report
if __name__ == "__main__":
data = full_check("example.com")
print("expires:", data["whois"].get("expiration_date"))
print("ssl:", data["ssl"])
print("takeover:", data["subdomain-takeover"])
print("threat:", data["threat-score"])
Swap in your domains, wrap it in cron, alert at 30, 14, and 7 days.
My current stack
Nightly cron against the RapidAPI free tier for the WHOIS, SSL, and takeover sweep, with raw RDAP as a fallback when a TLD misbehaves. Alerts go to a webhook that pings my phone. It's cost me $0 so far, and I haven't lost a domain since.
The squatter still has my old domain, by the way. The listing dropped from $4,800 to $2,200 last I checked. I'm not paying, out of pure spite, which is petty and feels great.
If you're comparing providers yourself, also look at WhoisXML API for bulk historical data and SecurityTrails when DNS history matters more than WHOIS. Both held up fine in my tests.
Which TLD has broken your WHOIS parsing the hardest? Drop it in the comments , I'll add it to the test set and rerun the numbers.
How do you currently handle VPN detection in your stack?
Top comments (0)