DEV Community

Tidy Tools
Tidy Tools

Posted on

Bulk WHOIS with RDAP: domain age, expiry and SPF/DMARC grade for 1,000 domains

If you manage more than a handful of domains, sooner or later one of these happens: a domain nobody remembered renewing expires, a client's domain loses its transfer lock, or someone asks "which of our 300 domains still have DMARC at p=none?"

Each of those has an answer in public data. The registry knows the expiry date and status codes. DNS knows the mail servers, SPF and DMARC policy. The problem is doing it for a list, not one domain at a time in a web form that rate-limits you after five lookups.

This post shows how to do a bulk lookup properly with RDAP (the JSON successor of WHOIS), what each registry actually publishes, and how to turn the result into an expiry and e-mail-security report.

WHOIS is being replaced by RDAP. That is good news.

Classic WHOIS is plain text over port 43, with a different format per registry. RDAP is the official replacement: HTTPS, JSON, standard field names. IANA publishes a bootstrap file that tells you which RDAP server is responsible for each TLD.

So a bulk lookup can be: find the registry's RDAP server from the IANA bootstrap (with rdap.org as a fallback), ask it, and fall back to classic WHOIS only for the country-code TLDs that have no RDAP yet (.jp, .io, .co, .it, .eu, .ru, .cn…). For .com and .net, the registry only keeps basic data, so one more request to the registrar's RDAP server gets the registrant organization and country when they are public.

That is exactly what Bulk WHOIS Lookup & DNS Lookup - Domain Age & Expiry does, together with DNS over HTTPS for the records.

One row per domain

Input can be domains, URLs or even e-mail addresses. Everything is reduced to the registrable domain and de-duplicated:

{ "domains": ["github.com", "www.github.com", "https://www.bbc.co.uk/news", "jane@shop.example.de"], "checkAvailability": true }
Enter fullscreen mode Exit fullscreen mode

github.com and www.github.com become one row (and are charged once). A real result from September 2026, with TXT records shortened:

{
  "domain": "github.com",
  "inputs": ["github.com", "www.github.com"],
  "registered": true,
  "registrar": "MarkMonitor Inc.",
  "createdAt": "2007-10-09T18:20:50Z",
  "expiresAt": "2028-10-09T18:20:50Z",
  "daysToExpiry": 741,
  "domainAgeDays": 6930,
  "status": ["clientDeleteProhibited", "clientTransferProhibited", "clientUpdateProhibited"],
  "dnssec": false,
  "registrantOrg": "GitHub, Inc.",
  "registrantCountry": "US",
  "registrationSource": "rdap",
  "registrationServer": "rdap.verisign.com",
  "dns": {
    "a": ["140.82.112.4"],
    "mx": [{ "priority": 0, "host": "github-com.mail.protection.outlook.com" }],
    "caa": [{ "flags": 0, "tag": "issue", "value": "digicert.com" }, { "flags": 0, "tag": "issue", "value": "letsencrypt.org" }],
    "dmarc": "v=DMARC1; p=quarantine; sp=reject; pct=100; rua=mailto:dmarc@github.com; ruf=mailto:dmarc@github.com; fo=1"
  },
  "emailSecurity": {
    "grade": "B",
    "spfPolicy": "softfail",
    "dmarcPolicy": "quarantine",
    "spf": { "policy": "softfail", "includes": ["spf.protection.outlook.com", "_netblocks.google.com", "..."], "dnsLookups": 8, "multipleRecords": false },
    "issues": [],
    "note": "DKIM is not checked (it needs the sender's selector)."
  }
}
Enter fullscreen mode Exit fullscreen mode

In one row: about 19 years old, expires in 741 days, transfer-locked, mail on Microsoft 365, SPF at 8 of the 10 allowed DNS lookups, DMARC at quarantine. That dnsLookups: 8 is the kind of thing that silently breaks SPF when someone adds one more include:.

The e-mail security grade

Every domain gets an A–F grade from its SPF and DMARC records:

  • A: SPF (-all or ~all) and DMARC p=reject
  • B: DMARC p=quarantine (or reject for only part of the mail, or testing mode)
  • C: DMARC p=none
  • D: only SPF or only DMARC, or a weak SPF (?all, +all)
  • F: neither

SPF include: and redirect= are followed the way a mail server does it, up to the 10-lookup limit. Every grade below A comes with at least one entry in issues (no SPF, two SPF records, p=none, too many lookups…). Domains with no MX that publish v=spf1 -all and DMARC reject also get an A: they are locked against spoofing, which is exactly what a parked domain should do.

Use case 1: an expiry report for a domain portfolio

Run your list weekly with an Apify schedule, then filter:

from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
domains = [line.strip() for line in open("domains.txt") if line.strip()]

run = client.actor("tidytools/bulk-domain-whois-dns-lookup").call(run_input={"domains": domains})

for d in client.dataset(run["defaultDatasetId"]).iterate_items():
    if not d.get("success"):
        continue
    days = d.get("daysToExpiry")
    unlocked = "clientTransferProhibited" not in (d.get("status") or [])
    if (days is not None and days < 60) or unlocked or d.get("emailGrade") in ("D", "F"):
        print(d["domain"], days, "unlocked" if unlocked else "", d.get("emailGrade"))
Enter fullscreen mode Exit fullscreen mode

Or call it from anything that speaks HTTP:

curl -X POST "https://api.apify.com/v2/acts/tidytools~bulk-domain-whois-dns-lookup/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domains":["github.com","bbc.co.uk"],"checkAvailability":true}'
Enter fullscreen mode Exit fullscreen mode

Use case 2: an e-mail deliverability audit for clients

Agencies and MSPs can paste every client domain and sort by emailGrade. The SUMMARY record counts domains per grade, so "12 of our 80 clients have no DMARC" is one number you can put in a report. It also accepts another run's dataset (datasetId + datasetField), so the domains from a lead or contact scraper can be checked without exporting anything.

Use case 3: a first pass on domain name ideas

With checkAvailability on, a name nobody has registered looks like this:

{ "domain": "this-domain-surely-not-registered-xyz123.com", "registered": false, "availability": "likely_available",
  "availabilityNote": "Guess: the registry has no record of this domain. Premium, reserved or blocked names can still be unavailable; confirm with a registrar.",
  "registrationSource": "rdap", "dnsResolves": false }
Enter fullscreen mode Exit fullscreen mode

It is deliberately worded as a guess. When the registry could not be reached, the answer is unknown, never likely_available.

What registries actually publish

This surprised me most while building it. "WHOIS data" is not one thing; it depends on the registry. From a test of 20 domains in September 2026:

TLD Source What came back
.com, .net, .org, .dev, .app, .fr RDAP Full: registrar, dates, status, name servers; registrant org/country when public
.co.uk RDAP Registrar, dates, status, name servers
.de (DENIC) RDAP Status and name servers only: no dates, registrar or holder
.com.au, .ch RDAP Registrar, status, name servers; no dates
.io, .co WHOIS Full, including registrant org and country when public
.jp WHOIS Dates, registrant, name servers; no registrar
.eu WHOIS Registrar and name servers only
.es none No RDAP, and WHOIS only answers pre-registered IPs: no registration data (DNS still works)

So if your portfolio is mostly .de, an "expiry report" from public data is not possible for those domains, whichever tool you use. When registration data cannot be read, the row still has the DNS records and a registrationError.

Two more honest limits: most registrant names are redacted under GDPR (values like "REDACTED" come back as null), and DKIM is not checked because it needs the sender's selector.

What it costs

$2.00 per 1,000 domains on the Free and Starter plans ($1.80 on Scale, $1.60 on Business and higher). No start fee. 10,000 domains cost $20.

A domain is charged when registration data or DNS records came back (a registry saying "not registered" is a valid answer and counts). Lookups where both RDAP/WHOIS and DNS failed, invalid lines and duplicates are free. The run logs its worst case before starting and stops cleanly at your maximum charge per run.

Actor: https://apify.com/tidytools/bulk-domain-whois-dns-lookup


Disclosure: I built this Actor and earn from its usage on Apify. github.com and the other domains are public examples from my tests in September 2026 and have no connection to me.

Top comments (0)