If you keep a portfolio of domains, watch competitors' domains, or want to know which names on a shortlist are still free, checking them one by one in a WHOIS web form does not scale. Command-line whois output is free text that differs per registry, so parsing it is its own project. A cleaner route is RDAP, the JSON successor to WHOIS that registries publish, combined with a few DNS queries.
This post shows how to run that for a list of domains with an Apify actor I built, Bulk Domain WHOIS (RDAP) and DNS Lookup, and then triage the results in Python. Disclosure: it's my actor and I earn a little when it's used. It only wraps public RDAP and DNS, so you could also build it yourself.
Price
Pay-per-event: $0.002 per domain looked up at the time of writing (so $2 per 1,000). Errors, invalid names and unknown results are not charged. Duplicates in your list are removed. Max 1000 domains per run.
Input
{
"domains": ["github.com", "python.org", "stripe.com", "expired-example-zzqx-48213.com", "notadomain", "https://www.wikipedia.org/page"],
"maxConcurrency": 10
}
URLs are accepted and reduced to the host name (see wikipedia.org below).
Real output
I ran the actor's source locally on exactly that input on 2026-10-16. Three of the six rows, trimmed (the full output also has a txt field and a checkedAt timestamp):
[
{
"domain": "stripe.com",
"available": false,
"registrar": "SafeNames Ltd.",
"created": "1995-09-12T04:00:00Z",
"expires": "2027-09-11T04:00:00Z",
"ageDays": 11357,
"status": ["client delete prohibited", "client transfer prohibited", "..."],
"nameservers": ["ns-1087.awsdns-07.org", "ns-1882.awsdns-43.co.uk", "..."],
"a": ["198.137.150.231", "198.202.176.231"],
"mx": ["10 aspmx.l.google.com", "20 alt1.aspmx.l.google.com", "..."],
"source": "rdap"
},
{
"domain": "expired-example-zzqx-48213.com",
"available": true,
"registrar": null,
"created": null,
"expires": null,
"ageDays": null,
"status": [],
"nameservers": [],
"a": [],
"mx": [],
"source": "rdap"
},
{
"domain": "notadomain",
"available": null,
"error": "Not a valid domain name"
}
]
So: a registered domain has registrar, created, expires, ageDays, nameservers, mx. An unregistered one has available: true and empty fields. An invalid name has an error and available: null. mx entries are "priority host" strings.
Caveat: available: true means the registry returned no record for the name. It is a good first filter, not a guarantee you can buy it (premium or reserved names exist), so confirm at a registrar before relying on it.
Call it from code
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("pennydata/bulk-domain-whois-dns-lookup").call(
run_input={"domains": ["github.com", "python.org", "stripe.com"], "maxConcurrency": 10}
)
rows = client.dataset(run["defaultDatasetId"]).list_items().items
Triage: free names, soon-to-expire domains, errors
This function reads only fields that appear in the output above. I ran it on the saved dataset from that run:
from datetime import datetime, timezone
def triage(rows, days=365):
now = datetime.now(timezone.utc)
free, expiring, errors = [], [], []
for r in rows:
if r.get("error"):
errors.append((r["domain"], r["error"]))
elif r["available"]:
free.append(r["domain"])
elif r["expires"]:
exp = datetime.fromisoformat(r["expires"].replace("Z", "+00:00"))
left = (exp - now).days
if left <= days:
expiring.append((r["domain"], left, r["registrar"]))
return free, sorted(expiring, key=lambda t: t[1]), errors
free, expiring, errors = triage(rows, days=700)
On 2026-10-16 with days=700 it returned:
free: ['expired-example-zzqx-48213.com']
expiring: [('wikipedia.org', 88, 'MarkMonitor Inc.'), ('stripe.com', 329, 'SafeNames Ltd.')]
errors: [('notadomain', 'Not a valid domain name')]
(The day counts depend on the date you run it. A domain showing 88 days left may well be auto-renewed; expires is the registry's date, not a promise it will lapse.)
Notes and limits
- Data comes from the registry's RDAP server, so which fields exist (and whether registrar names are redacted) varies by TLD. Don't assume every TLD fills every field.
- I tested six domains, all
.comor.org. I did not test large batches or rarer TLDs for this post, so check a sample of your own list first. - To monitor a portfolio, schedule the actor on Apify and diff
expiresandnameserversbetween runs.
Written with AI (Claude).
Top comments (0)