# Bulk WHOIS & DNS Lookup - Domain Checker, MX, SPF, DMARC (`neverempty/domain-lookup`) Actor

Registration date, expiry, registrar, nameservers, mail servers, SPF, DMARC and the TLS certificate for any domain - one row each. Reads ICANN RDAP and public DNS directly: no API key, no proxies. Says why a value is missing instead of reporting a domain as unregistered when it could not ask.

- **URL**: https://apify.com/neverempty/domain-lookup.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Lead generation, Developer tools, MCP servers
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 domain checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bulk WHOIS & DNS Lookup - Domain Checker, MX, SPF, DMARC

Give it a list of domains. Get back one row per domain with the registration record, the nameservers, the mail servers, the SPF and DMARC policy and the TLS certificate — and, when something is missing, the reason it is missing.

No API key. No proxy cost. Nothing that breaks when a website is redesigned, because this reads registry and DNS data directly rather than scraping web pages.

### Three things this does that a plain lookup does not

**1. It goes to the registry that actually owns the answer.**
Registration data comes from ICANN RDAP. Instead of routing every request through one public redirector, this reads the official IANA bootstrap file (`https://data.iana.org/rdap/dns.json`, RFC 7484 — 590 services, published 2026-07-23) and queries the RDAP server that is responsible for that TLD.

Measured on the same 20 domains: going through a single shared redirector returned registration data for **5 of 20**, because the shared endpoint answered `HTTP 429` for the rest. Querying the responsible registry directly returned **17 of 20** — and named the reason for the other three.

**2. It never reports a domain as unregistered when it simply could not ask.**
`registered` is `true`, `false` or `null`. `null` means "not answered", and `registrationLookupError` says why. The three reasons measured in testing:

| What happened | What the row says |
|---|---|
| The TLD publishes no RDAP service at all (`.edu`, `.eu`) | `.edu does not publish RDAP (no responsible server in the IANA bootstrap)` |
| The registry answered but omitted the dates (`gov.uk`) | `the registry does not publish a registration date for this domain` |
| The lookup failed | the transport error, after retries |

`gov.uk` is a real example: the same registry returns a 1994 registration date for `bbc.co.uk` and no date at all for `gov.uk`. A row that just showed a blank cell would be indistinguishable from a bug.

**3. RDAP only answers for registrable domains — so it tells you which domain the answer is about.**
Ask about `expired.badssl.com` and RDAP returns 404, because RDAP has no record for subdomains. Reporting that as "not registered" would be wrong. This walks up to the registrable parent and reports it in `rdapDomain`, so `registered: true, rdapDomain: badssl.com` is unambiguous.

### Columns you get that a raw lookup does not

| Column | Why it is there |
|---|---|
| `domainAgeDays` | Age in days, computed from the registration event. Useful for scoring a lead list — a domain registered three weeks ago is a different risk from one registered in 1998. |
| `daysUntilExpiry` | Days until the registration lapses. |
| `dnsProvider` / `mailProvider` | Who actually runs the DNS and the mailboxes, read from the nameserver and MX hostnames (Cloudflare, AWS Route 53, Akamai, Google Workspace, Microsoft 365, Proofpoint, Mimecast and others). `null` when the hostnames do not identify a known provider — never a guess. |
| `spoofProtection` | `blocked` / `quarantined` / `monitored` / `open`. SPF alone does not stop someone forging the visible From address, so this is derived from the DMARC policy, which is what receivers actually enforce. |
| `spfDuplicates` | Number of SPF records when there is more than one. Two SPF records is a misconfiguration that silently breaks SPF. |
| `tlsExpired` / `tlsMatchesDomain` | Whether the certificate has expired, and whether it actually covers this hostname — wildcards handled at one label, as the specification requires. |

### A real row from a real run

Values below are exactly what the Actor returned for `cloudflare.com` on 2026-08-26. Long arrays and the full SPF record are shortened here for readability; the dataset carries them in full.

```json
{
  "domain": "cloudflare.com",
  "ok": true,
  "registered": true,
  "rdapDomain": "cloudflare.com",
  "registrationLookupError": null,
  "registrar": "Cloudflare, Inc.",
  "registeredOn": "2009-02-17T22:07:54Z",
  "expiresOn": "2033-02-17T22:07:54Z",
  "domainAgeDays": 6398,
  "daysUntilExpiry": 2368,
  "nameservers": ["ns5.cloudflare.com", "ns6.cloudflare.com", "ns7.cloudflare.com"],
  "dnsProvider": "Cloudflare",
  "ipv4": ["104.16.133.229", "104.16.132.229"],
  "hasMx": true,
  "mailProvider": "Cloudflare Email Security",
  "spfStrictness": "hardfail",
  "spfDuplicates": 0,
  "dmarcPolicy": "reject",
  "spoofProtection": "blocked",
  "tlsIssuer": "Google Trust Services",
  "tlsValidTo": "2026-10-06T22:47:27.000Z",
  "tlsDaysUntilExpiry": 42,
  "tlsExpired": false,
  "tlsMatchesDomain": true,
  "tlsError": null,
  "checkedAt": "2026-08-26T02:31:41.665Z"
}
```

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `domains` | array of strings | five well-known domains | Domains to look up. Full URLs and email addresses are accepted — `https://www.example.com/pricing` and `sales@example.com` both become `example.com`. Internationalised names are converted the way a browser does: `bücher.de` becomes `xn--bcher-kva.de`, `日本語.jp` becomes `xn--wgv71a119e.jp`, whether you write them plain or as a URL. Anything that is not a domain is returned with a reason instead of being dropped. |
| `checkRdap` | boolean | `true` | Look up registrar, registration date, expiry date and registry status via RDAP. |
| `checkTls` | boolean | `true` | Connect to port 443 and read the certificate. |
| `timeoutSecs` | number | `20` | Timeout for one RDAP request or one TLS handshake. Range 3–120. |
| `maxRetries` | number | `3` | Retries for temporary failures — timeout, connection reset, `HTTP 429`, `HTTP 5xx`. A domain that genuinely does not exist is never retried, because that is already an answer. Range 1–10. |

### Speed and cost

Measured runs: **5 domains in 1.8 s**, **20 domains in 9.7 s**, including the one-off IANA bootstrap fetch (71 KB, reused for every domain in the run). There is no proxy traffic and no third-party API key, so the only cost is compute time.

You are charged per domain that produced an answer. Input that could not be parsed as a domain is returned with a reason and is not charged.

### Typical uses

- **Scoring a lead list.** Domain age, whether the domain is about to expire, and whether it has real mailboxes separate live businesses from parked domains.
- **Email deliverability review.** SPF strictness, duplicate SPF records and the DMARC policy, for a whole customer list at once.
- **Certificate watch.** `tlsDaysUntilExpiry` and `tlsMatchesDomain` across every domain you own, before a browser warning does it for you.
- **Vendor and acquisition checks.** Registrar, registry status locks, DNS and mail providers in one table.

### FAQ

**Why is `registered` sometimes `null`?**
Because the answer is unknown, not negative. `registrationLookupError` says whether the TLD publishes no RDAP at all, whether the registry withheld the dates, or whether the lookup failed.

**Does this check whether a mailbox exists?**
No. It reports the mail infrastructure — MX hosts, provider, SPF, DMARC. Verifying a specific address requires an SMTP conversation, which this does not do, so it does not claim to.

**Which TLDs have registration data?**
Every TLD listed in the IANA bootstrap file — 590 services at the time of writing. `.edu`, `.eu`, `.jp` and `.io` are examples that publish no RDAP service; for those, DNS and TLS data is still returned and the registration columns carry the reason.

**Does it handle internationalised domain names?**
Yes. `bücher.de`, `münchen.de` and `日本語.jp` are converted to their punycode form before lookup, exactly as a browser would, and the converted name is what appears in the `domain` column. Writing the same name plainly or as a URL gives the same answer.

**Does it work on subdomains?**
Yes. DNS and TLS are checked on the exact hostname you supply; registration data comes from the registrable parent, named in `rdapDomain`.

### Other tools by NeverEmpty

Every NeverEmpty Actor follows the same rule: it never returns an empty result to mean two different things, and it only charges for rows that actually carry an answer.

- **[website-contacts](https://apify.com/neverempty/website-contacts)** - emails, phones and social profiles from any website, no invented addresses
- **[link-checker](https://apify.com/neverempty/link-checker)** - broken links on a site, with blocked told apart from dead
- **[seo-site-audit](https://apify.com/neverempty/seo-site-audit)** - technical SEO audit: meta, canonical, robots, headings

### Support

Found a wrong value or a domain that behaves oddly? Open an issue on the Issues tab of this Actor with the domain and the row you got. Reports about incorrect data are fixed first.

# Actor input Schema

## `domains` (type: `array`):

Domains to look up. Full URLs and email addresses are accepted too - 'https://www.example.com/pricing' and 'sales@example.com' both become 'example.com'. Anything that is not a domain is reported with a reason instead of being silently skipped.

## `checkRdap` (type: `boolean`):

Ask ICANN RDAP for the registrar, registration date, expiry date and registry status. RDAP only answers for registrable domains, so for a subdomain the parent domain is queried and reported in the 'rdapDomain' column. Turn off to make runs faster when you only need DNS and TLS.

## `checkTls` (type: `boolean`):

Connect to port 443 and read the certificate: issuer, validity dates, whether it has expired and whether it actually covers this domain. Turn off to skip the connection.

## `timeoutSecs` (type: `number`):

How long to wait for one RDAP request or one TLS handshake before giving up on it.

## `maxRetries` (type: `number`):

How many times to retry a lookup that failed for a temporary reason (timeout, connection reset, HTTP 5xx). A domain that genuinely does not exist is never retried - that is already an answer.

## Actor input object example

```json
{
  "domains": [
    "mozilla.org",
    "github.com",
    "wikipedia.org",
    "cloudflare.com",
    "shopify.com"
  ],
  "checkRdap": true,
  "checkTls": true,
  "timeoutSecs": 20,
  "maxRetries": 3
}
```

# Actor output Schema

## `results` (type: `string`):

All domains produced by this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "mozilla.org",
        "github.com",
        "wikipedia.org",
        "cloudflare.com",
        "shopify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/domain-lookup").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "domains": [
        "mozilla.org",
        "github.com",
        "wikipedia.org",
        "cloudflare.com",
        "shopify.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/domain-lookup").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "mozilla.org",
    "github.com",
    "wikipedia.org",
    "cloudflare.com",
    "shopify.com"
  ]
}' |
apify call neverempty/domain-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/domain-lookup"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/RPKLFCnjvak7knzqH/builds/LE4dt2RgkzjqCcqnI/openapi.json
