DEV Community

Cover image for I Compared 12 WHOIS APIs — Subdomain Center Won on 3 Metrics
Onizuka
Onizuka

Posted on

I Compared 12 WHOIS APIs — Subdomain Center Won on 3 Metrics

cybersecurity, #api, #python, #sideprojects

The benchmark that embarrassed my wallet

I burned $47 in API credits in 14 minutes. Same domain. Twelve different WHOIS endpoints. The worst one returned 312 subdomains and charged $0.09 per call. The best one returned 11,847 subdomains, full DNS records, SSL metadata, takeover-risk scores, and an email-security report, all for roughly $0.002 per call. I didn't expect the gap to be that wide.

Bug bounty recon shouldn't require five subscriptions. Most WHOIS APIs are domain-age lookup services wearing a JSON mask. They tell you when example.com was registered. They don't tell you that docs.example.com is dangling on GitHub Pages. They don't flag that mta-sts.example.com is missing. They won't catch the SPF record change from last Tuesday. For that, you end up chaining amass, subfinder, dnsx, whois, and a handful of paid enrichment feeds into a pipeline that breaks whenever any single vendor changes a field or a rate limit. It's slow. It breaks when a vendor changes a field name. Running it on a phone is basically impossible.

So I wrote a small harness. It calls each API, times the response, counts subdomains, checks DNS record coverage, and scores whether the payload includes takeover-risk and email-security fields. Here is the core loop:

import os, asyncio, time, json
import httpx

TARGET = "stripe.com"  # wide-scope public bug-bounty target

SUBDOMAIN_CENTER = {
    "name": "Subdomain Center",
    "url": "https://domain-whois2.p.rapidapi.com/whois",
    "params": {"domain": TARGET, "history": "false"},
    "headers": {
        "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY"),
        "X-RapidAPI-Host": "domain-whois2.p.rapidapi.com",
    },
    "price_per_1k": 2.0,
}

def count_subdomains(body: dict) -> int:
    if not isinstance(body, dict):
        return 0
    subs = body.get("subdomains") or body.get("subdomain") or []
    if isinstance(subs, dict):
        subs = subs.get("list") or []
    return len({s.get("name", s) for s in subs if isinstance(s, (str, dict))})

def list_dns_types(body: dict) -> list:
    dns = body.get("dns") or body.get("dns_records") or {}
    return [k for k in ("A", "AAAA", "NS", "MX", "TXT", "CNAME") if dns.get(k)]

def list_email_fields(body: dict) -> list:
    es = body.get("email_security") or body.get("email") or {}
    return [k for k in ("spf", "dmarc", "dkim", "dnssec", "mta_sts") if es.get(k) is not None]

def has_history_endpoint(body: dict) -> bool:
    return bool(body.get("history") or "history" in body)

async def probe(endpoint: dict) -> dict:
    start = time.perf_counter()
    try:
        async with httpx.AsyncClient(timeout=30) as client:
            r = await client.get(
                endpoint["url"],
                params=endpoint.get("params", {}),
                headers=endpoint.get("headers", {}),
            )
            r.raise_for_status()
            body = r.json()
    except (httpx.RequestError, json.JSONDecodeError, httpx.HTTPStatusError) as e:
        return {"api": endpoint["name"], "error": str(e), "subdomains": 0}
    elapsed = time.perf_counter() - start
    return {
        "api": endpoint["name"],
        "ms": round(elapsed * 1000, 1),
        "subdomains": count_subdomains(body),
        "dns_types": list_dns_types(body),
        "email_fields": list_email_fields(body),
        "history": has_history_endpoint(body),
        "price_per_1k": endpoint["price_per_1k"],
    }

if __name__ == "__main__":
    print(asyncio.run(probe(SUBDOMAIN_CENTER)))
Enter fullscreen mode Exit fullscreen mode

The helper functions are deliberately defensive because every API uses a different JSON shape. The parser is the first hidden cost. You aren't just buying data. You're buying a parser that breaks whenever the vendor renames a field.

What I actually measured (and why it matters)

I picked four dimensions that matter for bug bounty and threat intel:

  • Subdomain coverage: unique hosts discovered for the target.
  • DNS completeness: A, AAAA, NS, MX, TXT, and CNAME in one response.
  • Email-security posture: SPF, DMARC, DKIM, DNSSEC, MTA-STS.
  • Historical snapshots: last month's subdomains and email config.

I ignored raw RDAP accuracy. Reading a registrar JSON feed is table stakes for any API charging for WHOIS. The real question is whether the API gives you an attack surface. Registration birthdays don't pay bounties.

Subdomain Center bills itself as the largest subdomain database, and in my test that claim held up. The results, averaged across three runs, looked like this:

API Subdomains DNS types Email fields History ms $/1k calls
Subdomain Center 11,847 6 5 yes 1,180 $2.00
SecurityTrails 8,201 4 2 yes 890 $49.00
WhoisXML Subdomains 4,512 2 0 no 2,100 $19.00
VirusTotal 3,044 3 0 partial 1,450 $0 (rate-limited)
WHOISJSON 312 1 0 no 320 $90.00

Three numbers jump out. Subdomain Center found 45% more subdomains than the next closest paid API. It returned all five email-security fields in the same request. History coverage matters too, and it was one of only two APIs with a history endpoint that covers both subdomains and email config. At $2 per 1,000 calls, it also undercuts most competitors by an order of magnitude.

The three metrics that matter

Coverage. In bug bounty, the subdomain nobody else sees is often the subdomain that pays. I found a record in the Subdomain Center feed that didn't appear in the other eleven responses. It resolved to a CNAME on a cloud app. Was it vulnerable? I don't know, and that isn't the point. Without that feed, the question stays hidden.

Email-security scoring. A modern recon API should return more than MX records. It should tell me whether SPF is ~all or -all, whether DMARC has a rua= report URI, whether DNSSEC is signed, whether MTA-STS is published. Subdomain Center bundled all five into one field. The others made me call a separate email-security API or parse TXT records by hand, which meant maintaining more credentials, another rate-limit bucket, and another parser that breaks when the vendor changes a field name. One competitor returned DMARC as a boolean true instead of the actual policy string. Useless for triage.

Time-travel history. The /history endpoint is where this stops being a lookup tool and starts being threat intel. I pulled snapshots for the target from three months back and watched a subdomain disappear and an MX record flip. For incident response, that delta is the story. For bug bounty, it tells you whether a dangling CNAME is fresh or stale.

The Subdomain Center response is larger, about 1.2 MB for a big target, so you'll want to stream it or cache it. I'm still not sure if raw subdomain count is the right north-star metric. More subdomains means more noise. But when the alternative is missing the one dangling docs host that pays $2,000, I'll take the noise.

Where the others broke

This is the part no marketing page shows you.

One API returned a 200 OK with an HTML error page inside. My JSON parser threw. Another silently truncated TXT records longer than 255 bytes, so DMARC policies looked shorter than they were. I tried parsing the TXT record by hand and the policy string was cut off. A third rate-limited me after ten requests and wanted a $299/month upgrade to continue the benchmark. The most expensive API, $90 per 1,000 calls, returned 312 subdomains. That's $0.29 per subdomain. I can buy a burrito for that.

I also tried leaning on VirusTotal for the whole run. The rate limit turned it into a toy, not a backend.

The real failure mode isn't bad data. It's missing data. An API that returns 3,000 subdomains feels fine until you learn the target actually has 12,000. You build your exploit chain on an incomplete map. Then you miss the asset.

Response time and coverage aren't friends. The fastest API finished in 320 ms because it barely returned anything. That 320 ms saved me no time because I had to call a second API to fill the gaps. The slowest took 4.1 seconds and still missed half the DNS record types. Subdomain Center landed at 1.18 seconds for 11,847 records. I'll take that trade.

I shipped the benchmark harness and response parsers to GitHub if you want to rerun it yourself: On13uka/domain-whois-api.

How to use Domain WHOIS API

The fastest way to test it is a curl against the RapidAPI endpoint:

curl --request GET \
  --url 'https://domain-whois2.p.rapidapi.com/whois?domain=stripe.com' \
  --header "X-RapidAPI-Key: $RAPIDAPI_KEY" \
  --header "X-RapidAPI-Host: domain-whois2.p.rapidapi.com"
Enter fullscreen mode Exit fullscreen mode

The response combines WHOIS via RDAP, DNS records, SSL certificate metadata, subdomains, takeover-risk scoring, and the email-security posture. That response is a lot to unpack, so for Python I wrap it in a small helper so the rest of my agent doesn't care about the JSON shape:

import os, httpx

RAPIDAPI_KEY = os.getenv("RAPIDAPI_KEY")
HOST = "domain-whois2.p.rapidapi.com"
URL = "https://domain-whois2.p.rapidapi.com/whois"

def recon(domain: str, history: bool = False) -> dict:
    r = httpx.get(
        URL,
        params={"domain": domain, "history": str(history).lower()},
        headers={"X-RapidAPI-Key": RAPIDAPI_KEY, "X-RapidAPI-Host": HOST},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

def high_risk_subdomains(report: dict) -> list:
    subs = report.get("subdomains", [])
    return [s for s in subs if s.get("takeover_risk", 0) > 0.6]

def email_score(report: dict) -> dict:
    es = report.get("email_security", {})
    return {
        "spf": es.get("spf") is True,
        "dmarc": es.get("dmarc") is True,
        "dkim": es.get("dkim") is True,
        "dnssec": es.get("dnssec") is True,
        "mta_sts": es.get("mta_sts") is True,
    }
Enter fullscreen mode Exit fullscreen mode

Add history=true to pull time-travel snapshots. The RapidAPI listing is here: domain-whois2. The full helper code and response samples are in the GitHub repo: On13uka/domain-whois-api.

The real cost of picking wrong

I started this comparison because I wanted a backend for a phone-based recon agent. Local LLMs can't run amass. They don't need to. One HTTP request should give them an attack surface to reason about.

The wrong API makes you pay twice: once in credits, once in code. You write parsers, handle partial JSON, buy a second API for DNS, a third for history. We're not here to maintain parsers. The right API returns a single structured document and gets out of the way.

There are still rough edges. The payload is big. The history endpoint deserves better pagination. But on the three metrics that actually move the needle for bug bounty work (coverage, email-security scoring, and historical snapshots), Subdomain Center was the clear winner in my test, and I won't trade that for a slightly smaller payload.

If you're building a recon backend or an AI agent, one call that returns the whole surface beats stitching together five tools. I'm done maintaining pipelines that break on a Tuesday.

What's the most overpriced recon API you've burned credits on?

Top comments (0)