DEV Community

PennyData
PennyData

Posted on

Bulk domain check in Python: availability, expiry dates and registrar for a whole list

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
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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')]
Enter fullscreen mode Exit fullscreen mode

(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 .com or .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 expires and nameservers between runs.

Written with AI (Claude).

Top comments (0)