Bulk Domain Checker: Availability, Expiry, WHOIS, DNS & DMARC avatar

Bulk Domain Checker: Availability, Expiry, WHOIS, DNS & DMARC

Pricing

from $1.20 / 1,000 domains

Go to Apify Store
Bulk Domain Checker: Availability, Expiry, WHOIS, DNS & DMARC

Bulk Domain Checker: Availability, Expiry, WHOIS, DNS & DMARC

Check DNS, SPF/DMARC email-auth grade, RDAP registration & expiry and availability for any domain in one row. Official DoH and RDAP only. Monitor mode.

Pricing

from $1.20 / 1,000 domains

Rating

0.0

(0)

Developer

tessellware

tessellware

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 hours ago

Last modified

Features

Limited permissions

Share

Bulk-check domain availability, expiry dates, WHOIS (RDAP) registration data, DNS records and email authentication in one run. Give it a list of domains and get one row per domain with its DNS records, an email-authentication grade (A to F) from SPF, DMARC and friends, registration and expiry dates, and whether the name looks available. Everything comes from official, public protocols (DNS-over-HTTPS and RDAP): no scraping, no API keys, no paid data sources. A monitor mode returns only the domains that changed since your last run.

You are billed only for domain rows written to the dataset. Invalid inputs, duplicates, unchanged domains in monitor mode, and domains for which no requested check returned usable data are never written and never billed. TLDs without an RDAP server in the IANA bootstrap (for example .io and .de today) get no registration or expiry data; their availability comes from DNS only and may be unknown.

Maintained by Tessellware.

Quick start

  1. Paste domains, URLs or email addresses into Domains. The prefilled example (apify.com, github.com, example.com) finishes in a few seconds.
  2. Leave Checks to run empty for everything, or pick only the checks you need.
  3. Click Start, then open the Domains overview table or download the dataset as JSON, CSV or Excel.

How much does it cost to check domains in bulk?

You pay per domain row (see Pricing): $2.00 per 1,000 domains on the Free and Bronze tiers. Checking a 500-domain customer list once costs about $1.00; a nightly monitor over 500 domains pays only for the rows that changed. Set Max domains output (or the maximum cost per run in the Apify Console) to cap any run.

What you can do with it

  • Email deliverability audit: grade the SPF/DMARC setup of every sending domain you manage or sell to. Find domains with no DMARC, p=none, +all, multiple SPF records, or more than 10 SPF DNS lookups (which makes SPF fail).
  • Expiring-domain lists: rdap.expiresAt and rdap.daysToExpiry for a whole portfolio, sorted to show what to renew first.
  • Portfolio and competitor monitoring: schedule a run in monitor mode; you get a row only when nameservers, MX, SPF, DMARC, registrar, expiry, status flags or availability change, with a field-level changes list.
  • Lead-list hygiene: drop dead domains (nxdomain), parked-looking domains with no MX, and domains that cannot receive or authenticate mail, before you send to them.
  • Domain research: check whether a batch of candidate names is registered, with the caveats below.

How it works

For every domain the Actor:

  1. reduces the entry to its registrable domain using the Public Suffix List (https://www.bbc.co.uk/news becomes bbc.co.uk, jane@mail.example.org becomes example.org, münchen.de becomes xn--mnchen-3ya.de);
  2. queries DNS over HTTPS using the Cloudflare JSON API, falling back to Google Public DNS on errors and SERVFAIL (NXDOMAIN is treated as a real answer);
  3. finds the registry's RDAP server in the IANA bootstrap file (data.iana.org/rdap/dns.json, fetched once per run) and reads the domain's RDAP record;
  4. builds the row. One failed check never fails the row or the run; the reason is listed in the row's errors.

Requests are paced (RDAP servers: at most 3 at a time per host, with retry and back-off on 429 and 5xx, honours Retry-After (up to 30 s); HTTP redirects are followed only to public https hosts, at most 3 hops) and identify themselves honestly. Port-43 WHOIS, registrar website scraping and paid APIs are not used.

Input

Only domains is required.

{
"domains": ["apify.com", "https://www.github.com/apify", "billing@example.co.uk", "münchen.de"],
"checks": ["dns", "emailAuth", "rdap", "availability"],
"dkimSelectors": ["google", "selector1"]
}
FieldDefaultNotes
domainsrequiredBare domains, URLs, email addresses. De-duplicated by registrable domain; the first spelling is kept in input. Email inputs are reduced to their domain; the part before @ is never stored (input shows ***@example.org).
checksall fourdns, emailAuth, rdap, availability. A check you do not pick is absent from the row. The price per row does not change.
dkimSelectorsnoneDKIM selectors cannot be discovered, so you supply them. Each <selector>._domainkey.<domain> TXT record is looked up.
onlyChangesfalseMonitor mode, see below.
monitorNamedefaultOne baseline per name. Use a different name per list.
monitorIpChangesfalseCount A/AAAA changes as changes (CDN addresses rotate, so they are ignored by default).
maxDomains0 (no limit)Stop after this many rows are output.
concurrency10 (max 50)Domains checked in parallel.

Invalid entries (empty, IP addresses, bare public suffixes such as co.uk, unknown TLDs, malformed names) are not written to the dataset and not charged. They are listed with the reason in the INPUT_REPORT key-value record, which also shows how every accepted input was normalised. Email inputs are reduced to their domain; the part before @ is never stored, not even in INPUT_REPORT.

Output

One dataset item per domain. Example (shortened):

{
"domain": "github.com",
"input": "https://www.github.com/apify",
"tld": "com",
"publicSuffix": "com",
"status": "ok",
"dns": {
"a": ["20.207.73.82"],
"aaaa": [],
"mx": [{ "priority": 0, "host": "github-com.mail.protection.outlook.com" }],
"ns": ["dns1.p08.nsone.net", "ns-1283.awsdns-32.org"],
"txt": ["v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com ... ~all"],
"caa": [{ "flags": 0, "tag": "issue", "value": "digicert.com" }],
"soa": { "primaryNs": "dns1.p08.nsone.net", "serial": 1656468023 },
"cname": null
},
"emailAuth": {
"spf": { "record": "v=spf1 ... ~all", "valid": true, "allQualifier": "~all", "dnsLookupCount": 10, "issues": [] },
"dmarc": { "record": "v=DMARC1; p=quarantine; sp=reject; pct=100; ...", "policy": "quarantine", "subdomainPolicy": "reject", "pct": 100, "rua": ["mailto:dmarc@github.com"], "ruf": ["mailto:dmarc@github.com"], "issues": [] },
"mtaSts": false,
"tlsRpt": false,
"bimi": false,
"dkim": [],
"grade": "A"
},
"rdap": {
"registrar": "MarkMonitor Inc.",
"registrarIanaId": "292",
"createdAt": "2007-10-09T18:20:50.000Z",
"updatedAt": "2026-09-07T09:22:52.000Z",
"expiresAt": "2028-10-09T18:20:50.000Z",
"daysToExpiry": 694,
"statuses": ["client delete prohibited", "client transfer prohibited", "client update prohibited"],
"nameservers": ["dns1.p08.nsone.net", "ns-1283.awsdns-32.org"],
"dnssecSigned": false,
"rdapServer": "rdap.verisign.com"
},
"availability": "registered",
"checkedAt": "2026-11-15T12:00:00.000Z",
"errors": []
}

Field notes:

  • status: ok; nxdomain (the name does not exist in DNS: the row is still output and charged, because the answer is the result; emailAuth is null then); error is used internally when no requested check returned usable data (resolvers unreachable, or for example only rdap requested for a TLD without an RDAP server), and such a domain is not output and not charged (it is listed under failedDomainList in the OUTPUT record and in INPUT_REPORT).
  • availability: registered, likely-available, or unknown (see Limits).
  • tld is the last label (uk); publicSuffix is the full suffix (co.uk).
  • errors: real failures only, such as rdap: HTTP 429 ... or dns:CAA: SERVFAIL .... Other checks in the row are unaffected.
  • notes: informational messages that are not failures, for example rdap: no RDAP server for .io in the IANA bootstrap.
  • OUTPUT (key-value store): run statistics. INPUT_REPORT: every input and what happened to it.

Email-authentication grade

The grade uses only the published DNS records. It is deliberately simple and fully documented here, so you can reproduce it.

SPF pointsRule
2Exactly one SPF record, valid, ending in -all or ~all, with 10 or fewer DNS lookups
1A record exists but is flawed: ?all, no all, more than 10 lookups, multiple SPF records, or an include target that has no SPF record
0No SPF record, or +all
DMARC pointsRule
3p=reject or p=quarantine at pct=100 (the default when pct is absent)
2p=reject or p=quarantine with pct below 100
1p=none (monitoring only)
0No valid DMARC record

Grade by total: 5 = A, 4 = B, 3 = C, 1 or 2 = D, 0 = F. So A needs a valid SPF and an enforcing DMARC policy at 100%; F means no SPF (or +all) and no DMARC; SPF alone with no DMARC is a D. The grade is null when SPF or DMARC could not be looked up (the reason is in errors); this includes a lookup failure (for example SERVFAIL) on an include: or redirect= target, while an include that exists in DNS but has no SPF record is a policy flaw that still scores 1. Macro-based includes such as include:%{ir}.%{v}.%{d}.spf.example.com (used by some enterprise mail gateways) cannot be expanded without a sending IP: they are counted as one lookup but not followed, are listed in emailAuth.spf.macroTargets, and do not make the record invalid. The grade is computed from the rest of the record. DNS-lookup counting follows RFC 7208: include, a, mx, ptr, exists and redirect each count one, recursively, with a loop and depth guard. MTA-STS, TLS-RPT, BIMI and DKIM are reported but do not change the grade. The grade says nothing about whether a domain actually sends mail.

Monitor mode: only domains that changed

Turn on Monitor mode (onlyChanges) and give the list a Monitor name. The first run outputs every domain and stores a baseline in a named key-value store. Later runs output only the domains whose meaningful fields changed, each with a changes list:

{ "domain": "example.com", "isNew": false, "changes": [
{ "field": "emailAuth.dmarc.policy", "from": "none", "to": "reject" },
{ "field": "rdap.expiresAt", "from": "2027-08-13T04:00:00.000Z", "to": "2028-08-13T04:00:00.000Z" } ] }
  • Not compared: checkedAt, input, errors, notes, rdap.daysToExpiry (it changes daily), rdap.rdapServer, dns.soa (all fields: the serial changes with every zone edit and the primary nameserver flips between providers on multi-provider domains; SOA stays in the output), and A/AAAA addresses unless monitorIpChanges is on.
  • If a check failed in the new run (for example a transient RDAP 429), only the field that failed keeps its previous value, so an error is never reported as a change, and a real change that happens while a check is failing is still reported once the check works again.
  • The baseline of a changed domain is updated only after its row was delivered. A baseline that is merely out of date (nothing to report) is saved without delivering a row. Fields that failed earlier, sections you newly request, a different DKIM selector list and switching monitorIpChanges on or off are all filled in silently: they are never reported as a change and never charged.
  • A run with no changes writes nothing and charges nothing; the status message says so.

Limits

  • Availability is a signal, not a registry guarantee. likely-available means the registry's RDAP server answered "not found" and the name has no DNS delegation. Reserved names, premium names, names in redemption or pending-delete, and registry-specific rules are not modelled. Always confirm with a registrar before relying on it.
  • TLDs without an RDAP server in the IANA bootstrap get no registration or expiry data (rdap is null), and their availability comes from DNS only: registered if DNS shows nameservers or an SOA record, otherwise unknown. These TLDs give unknown availability (on 2026-11-15 this included .de and .io, among others; the list changes as registries add RDAP). If DNS shows nameservers or an SOA record, the domain is reported as registered instead. rdap is null and errors says there is no RDAP server.
  • Registries differ in which RDAP fields they publish: createdAt, updatedAt, registrar or dnssecSigned can be null. Some registries rate-limit RDAP; the Actor retries politely, so very large .com lists take longer.
  • Only the registrable domain is checked. mail.example.com is reduced to example.com; subdomain-level records are not inspected. DKIM needs selectors you supply.
  • DNS answers come from public resolvers and reflect their caches (typically up to a few minutes behind the authoritative servers).
  • The prefill runs in seconds, so the Apify daily auto-test passes comfortably.

Privacy

RDAP responses can contain registrant, administrative and technical contact details. This Actor never outputs them: names, emails, phone numbers and addresses of registrants and contacts are not read, stored or returned (a unit test with a full registrant vCard checks this). From the registry data it takes only the registrar's organisation name and IANA ID, dates, status flags, nameservers and the DNSSEC flag. Email inputs are reduced to their domain; the part before @ is never stored. No personal data is collected.

Pricing

$2.00 per 1,000 domains ($0.002 per domain row) on Apify's Free and Bronze discount tiers; Silver $1.60 / 1,000; Gold $1.20 / 1,000.

You pay per domain row written to the dataset (pay-per-event). Every domain row written is charged, including nxdomain and partially failed rows. Invalid inputs and domains for which no check returned usable data are never written and never charged. In monitor mode, only changed domains are written and charged. Apify's standard start charge also applies: $0.00005 per run for up to 1 GB of memory (the default 512 MB run costs $0.00005). There is no other start fee. Apify Store discount tiers are based on your total monthly spend on Apify.

Examples: a 500-domain list costs about $1.00 (about $0.80 on Silver); 10,000 domains cost about $20.00. A daily monitor over 500 domains where 5 domains change costs about $0.01 per day.

Integrations

Use the Apify API, schedules, webhooks, Make, Zapier, n8n or Google Sheets to feed domain lists in and send results or monitor alerts out. A typical set-up: a daily schedule with onlyChanges on and a webhook that posts non-empty datasets to Slack.

Use it from AI agents (MCP)

AI agents and assistants can find and run this Actor through the Apify MCP server (search-actors, then call-actor), for example to answer "which of these 200 prospect domains have no DMARC policy?" or "which of our domains expire in the next 60 days?". The input is a plain list of domains, and the output is one record per domain with a letter grade and dates, so it is easy for an agent to use. The Actor runs with Apify's limited permissions.

Other Actors from Tessellware

  • ATS Jobs Scraper: job postings from Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters and Recruitee in one schema, with an only-new-jobs monitor mode. Use it to find companies that are hiring, then check their domains here.

FAQ

Is it legal to check domains in bulk? It uses only public internet infrastructure designed for these lookups: DNS (through public DNS-over-HTTPS resolvers) and RDAP, the registries' official registration-data service. It does not scrape websites, log in or collect personal data (see Privacy). You are responsible for using the results lawfully, for example when you use them for outreach.

Is this WHOIS? It uses RDAP, the IETF-standard successor to WHOIS, served by the registries themselves. It does not query port 43 and does not scrape websites.

Why is a registrar or date missing? The registry did not publish it in RDAP. The field is null.

Does it output the domain owner? No, never. See Privacy.

Can I run it for thousands of domains? Yes. DNS is fast; the limiting factor is registry RDAP rate limits (3 parallel requests per RDAP host, backing off on 429). Use maxDomains and the run cost limit to cap spend.

What if the run is migrated or restarted? The Actor remembers which domains it already delivered and never writes the same domain twice, except in rare crash cases where the dataset cannot be read back: at most one batch may be written again (up to 20 rows).