# Domain WHOIS & DNS Lookup: Expiry, Registrar, MX & SPF (`pfistyle.digital/domain-whois-dns`) Actor

WHOIS + DNS in one: expiry, registrar, MX provider, SPF and DMARC. Monitor changes and expiring domains. $1.50 per 1,000 emitted domains; unreadable domains are free.

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

## Pricing

from $1.50 / 1,000 domains

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Domain WHOIS & DNS Lookup: Expiry, Registrar, MX & SPF

**$1.50 per 1,000 domains, WHOIS + DNS in one run.** Get registration date, expiry, registrar, nameservers, DNSSEC, IP addresses, email provider (MX), SPF and DMARC as flat rows, ready for spreadsheets, APIs and AI agents. Unreadable domains, unchanged domains and filtered results are free.

- **Fast:** 11 mixed domains in under 6 seconds (measured 2026-10-09); `.com` runs at about 2 domains per second.
- **Monitoring built in:** `onlyChanges` emits only domains whose registrar, expiry, nameservers, MX, SPF or DMARC changed; `expiringWithinDays` finds domains about to expire.
- **Privacy-safe:** registrar company only, never registrant or admin contact data.
- **Official sources:** RDAP (the ICANN successor to WHOIS) and public DNS over HTTPS. No browser, no proxy.

### Use cases

- Enrich company leads with their domain age and email provider.
- Monitor a domain portfolio for registry changes and upcoming expiry.
- Check SPF and DMARC policies and identify young domains for security or KYB reviews.
- Check availability: a registry 404 is an explicit `registered: false` result.

### Input

```json
{
  "domains": ["apify.com", "github.com", "bbc.co.uk"],
  "checks": ["registry", "dns"]
}
```

`domains` accepts a list or text separated by newlines, commas or semicolons. Domain names, URLs (`https://www.apify.com/pricing`), email addresses (`info@apify.com`), uppercase, trailing dots and IDNs (`müller.de`) work. Subdomains reduce to their registrable domain using the ICANN Public Suffix List: `news.bbc.co.uk` becomes `bbc.co.uk`. Duplicate normalized domains are removed; the first original input is retained. Invalid entries are skipped individually.

| Option | Default | Meaning |
|---|---|---|
| `checks` | `["registry", "dns"]` | `registry` (RDAP/WHOIS), `dns`, or both. Unknown values are ignored. |
| `onlyChanges` | `false` | Emit new domains or changes to monitored fields. |
| `previousDatasetId` | omitted | Read-only dataset for the comparison baseline. |
| `expiringWithinDays` | unset | Emit domains expiring within N days, including expired domains; unknown expiry is excluded. |
| `maxDomains` | unset | Limit unique valid domains after normalization. |

Empty input fails with a useful message. Individual invalid or unreadable domains do not stop the other lookups.

### Output

One flat row per emitted domain, with comma-separated lists. Real output for `apify.com` (2026-10-09):

```json
{
  "domain": "apify.com",
  "input": "https://www.apify.com/pricing",
  "registered": true,
  "tld": "com",
  "registrar": "Amazon Registrar, Inc.",
  "registrarIanaId": "468",
  "registeredAt": "2009-06-02",
  "expiresAt": "2035-06-02",
  "updatedAt": "2026-05-16",
  "ageDays": 6338,
  "daysUntilExpiry": 3158,
  "status": "client transfer prohibited",
  "nameservers": "ns-1225.awsdns-25.org, ns-1928.awsdns-49.co.uk, ns-449.awsdns-56.com, ns-839.awsdns-40.net",
  "dnssec": true,
  "a": "143.204.55.100, 143.204.55.60, 143.204.55.7, 143.204.55.88",
  "aaaa": "2600:9000:20a5:400:9:a03e:6540:93a1, 2600:9000:20a5:4200:9:a03e:6540:93a1, 2600:9000:20a5:4c00:9:a03e:6540:93a1, 2600:9000:20a5:5200:9:a03e:6540:93a1, 2600:9000:20a5:6200:9:a03e:6540:93a1, 2600:9000:20a5:ae00:9:a03e:6540:93a1, 2600:9000:20a5:d400:9:a03e:6540:93a1, 2600:9000:20a5:fa00:9:a03e:6540:93a1",
  "mx": "1 aspmx.l.google.com, 5 alt1.aspmx.l.google.com, 5 alt2.aspmx.l.google.com, 10 aspmx2.googlemail.com, 10 aspmx3.googlemail.com",
  "mailProvider": "google",
  "spf": "v=spf1 a mx include:_spf.google.com include:mailgun.org include:amazonses.com include:19497222.spf05.hubspotemail.net -all",
  "dmarcPolicy": "reject",
  "registrySource": "https://rdap.verisign.com/com/v1/",
  "registryNote": null,
  "checkedAt": "2026-10-09T14:35:28.132063Z",
  "changedFields": null
}
```

Dates are `YYYY-MM-DD`; `checkedAt` is UTC. Unknown fields are `null`. `registered` is true for a registry domain response or positive DNS answer, false for registry 404 or DNS NXDOMAIN without a registry result, and null when unclear. Registry results take precedence over DNS. NXDOMAIN for `_dmarc` alone does not mean the domain is available. DNS availability does not prove that a domain can be purchased.

`mailProvider` is one of `google`, `microsoft`, `zoho`, `proton`, `yandex`, `amazon-ses`, `mimecast`, `proofpoint`, `ovh`, `ionos`, `other`, `none`. MX priorities are retained and sorted numerically. `spf` is the whole SPF TXT record; `dmarcPolicy` is `none`, `quarantine`, `reject`, or null. `registryNote` is null, `no_rdap`, `limited_registry_data`, or `rdap_error`. `changedFields` is populated only when `onlyChanges` is enabled.

### Pricing

| Event | Price |
|---|---|
| `domain`: one emitted row | $0.0015 |
| Actor start, per GB of memory (minimum one event) | $0.00005 |

You pay only these events; platform usage is included. A row is charged if at least one requested check returned a result, including confirmed availability. Partial readable results are charged once. Invalid domains, duplicates, all-failed lookups, unchanged rows, expiry-filtered rows and domains beyond the configured limit have no domain charge. The cost limit is checked before every charge; the run stops cleanly when your limit is reached.

### Scheduled monitoring

Create an Apify Schedule, for example daily, and select a previous dataset:

```json
{
  "domains": ["apify.com", "github.com"],
  "checks": ["registry", "dns"],
  "onlyChanges": true,
  "expiringWithinDays": 30,
  "previousDatasetId": "PREVIOUS_DATASET_ID"
}
```

Set `previousDatasetId` to the last run's dataset ID (select it with the dataset picker). The selected dataset is only read and is never modified. Missing, invalid or inaccessible datasets produce a log warning and a `comparisonNotice` in `SUMMARY`; the run continues with duplicate suppression within the current run. Without a previous dataset, every distinct current row is eligible for output. No persistent history is maintained. A dataset containing only new/changed rows, a filtered result or a cost-interrupted result is an incomplete baseline: rows absent from it can be emitted again. For reliable repeated monitoring, compare with a complete snapshot dataset and update the input through your scheduler or integration; a static schedule does not automatically select the last run.

Domains are matched by normalized domain. Compared fields are `registered`, `registrar`, `expiresAt`, `status`, `nameservers`, `dnssec`, `mx`, `spf` and `dmarcPolicy`; A/AAAA and daily counters are excluded. `comparisonFields` and `comparisonHash` preserve the effective comparison snapshot, including known values retained after partial failures. Existing datasets without these fields are reconstructed from the output fields. Disable `onlyChanges` for recurring expiry reminders.

### Use with AI agents / MCP

```
https://mcp.apify.com?tools=pfistyle.digital/domain-whois-dns
```

Example prompt: *"Use pfistyle.digital/domain-whois-dns to check the registrar, expiry, email provider and DMARC policy for apify.com and github.com."*

Direct API call:

```bash
curl -X POST "https://api.apify.com/v2/acts/pfistyle.digital~domain-whois-dns/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
  -d '{"domains":["apify.com","github.com"],"checks":["registry","dns"]}'
```

### How it works

The actor downloads the IANA RDAP bootstrap once per registry-enabled run, falling back to the bundled copy. It adds DENIC (.de), SWITCH (.ch/.li) and Identity Digital (.io). RDAP is the public successor to WHOIS. DNS uses Cloudflare JSON DNS over HTTPS, switching to Google after three consecutive resolver failures and retaining the selected resolver for later queries. It requests A, AAAA, MX, TXT and `_dmarc` TXT; NS is queried only when RDAP supplied none.

Eight domain workers share thread-safe per-host rate limits. RDAP requests have a 30-second timeout and up to three retries with 2/4/8-second backoff for 429, 5xx and connection failures. RDAP 429 honors `Retry-After` seconds or an HTTP date, capped at 60 seconds, and halves that host's limit for the rest of the run (floor 0.2 requests/s). The cooldown applies across workers. DoH backs off on transport and resolver failures and switches after three failures. User-Agent: `DomainWhoisDnsReader/0.1`.

| Hosts | Requests/s per host |
|---|---|
| `cloudflare-dns.com`, `dns.google` | 10 |
| `rdap.verisign.com`, `pubapi.registry.google` | 2 |
| `rdap.publicinterestregistry.org`, `rdap.identitydigital.services`, `rdap.nominet.uk`, `rdap.nic.fr`, `rdap.sidn.nl`, `rdap.registro.br`, `rdap.cctld.au`, `rdap.ca.fury.ca` | 1 |
| `rdap.denic.de`, `rdap.nic.ch` | 0.5 |
| Other RDAP hosts | 1 |
| `data.iana.org` | One bootstrap request per run |

The Public Suffix List (ICANN section, [MPL-2.0](https://mozilla.org/MPL/2.0/)) and a copy of the IANA RDAP bootstrap are bundled. Private suffixes are excluded: `blog.github.io` becomes `github.io`.

### Which TLDs work

| Coverage | TLDs (tested 2026-10-09) |
|---|---|
| Full registry data + DNS | .com, .net, .org, .io, .ai, .uk (incl. .co.uk), .fr, .nl, .app, .dev, .ca, .au, .br, .in and every other TLD listed in the IANA RDAP bootstrap (1,200+, all generic TLDs) |
| Limited registry data + DNS | .de, .ch, .li (registration status and nameservers, usually no registrar or expiry) |
| DNS only | .co, .eu, .it, .es, .us, .at, .jp, .cn, .ru and other TLDs without RDAP (`registryNote: "no_rdap"`) |

### Known limitations

- What works: registrar, dates, status, nameservers and DNSSEC for all RDAP TLDs; A, AAAA, MX, SPF and DMARC for every TLD.
- Registrant, administrative, technical and billing contacts are never returned, by design.
- .de and .ch/.li registries publish no expiry or registrar; DNS-only TLDs have no registry dates. There is no port-43 WHOIS fallback yet.
- `registered: false` comes from a registry 404 or DNS NXDOMAIN; it does not guarantee the domain can be bought (premium or reserved names).
- SPF and DMARC are reported as published, not validated as a full email-security audit.

### Unreadable domains are free

Every run writes `SUMMARY` to its default key-value store. It contains counters `input`, `valid`, `output`, `charged`, `registered`, `notRegistered`, `unchanged`, `notExpiring`, `checked`; `no_rdap` counters by TLD; `rateLimited` counters by host; and `skipped` entries with `domain`, `reason`, `detail`. Reasons: `invalid_domain`, `lookup_failed`, `cost_limit`, `domain_limit`, `unchanged`, `not_expiring`. Registered/available counters describe checked domains, including those filtered from output; `valid` counts unique normalized domains before `maxDomains`.

Example `SUMMARY`:

```json
{"input": 3, "valid": 3, "output": 3, "charged": 3, "registered": 3, "notRegistered": 0, "unchanged": 0, "notExpiring": 0, "checked": 3, "skipped": [], "no_rdap": {}, "rateLimited": {}}
```

Status line: `3 domains delivered, 3 checked (3 registered, 0 available); skipped, not charged: 0 invalid, 0 failed`; with filters or a cost limit it adds e.g. `, 3 unchanged` or `; stopped at cost limit, 7 not processed`. A domain where no requested check returned a result produces no row and no charge; other domains continue.

# Actor input Schema

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

A list of domains or text separated by commas/newlines. URLs, email addresses, IDNs and subdomains are normalized; duplicates are removed.

## `checks` (type: `array`):

registry (RDAP), dns, or both.

## `onlyChanges` (type: `boolean`):

Compare with rows in previousDatasetId. Without a readable previous dataset, only duplicates within this run are suppressed.

## `expiringWithinDays` (type: `integer`):

Emit domains expiring within N days, including expired domains. Unknown expiry is skipped.

## `maxDomains` (type: `integer`):

Optional limit after normalization and deduplication.

## `previousDatasetId` (type: `string`):

Optional read-only dataset for comparison with a previous run. Set previousDatasetId to the last run's dataset ID. If absent or unreadable, only duplicates within this run are suppressed. A changes-only dataset is not a complete historical baseline.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "github.com",
    "bbc.co.uk"
  ],
  "checks": [
    "registry",
    "dns"
  ],
  "onlyChanges": false
}
```

# Actor output Schema

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

Flat domain registry and DNS rows.

# 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": [
        "apify.com",
        "github.com",
        "bbc.co.uk"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pfistyle.digital/domain-whois-dns").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": [
        "apify.com",
        "github.com",
        "bbc.co.uk",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("pfistyle.digital/domain-whois-dns").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": [
    "apify.com",
    "github.com",
    "bbc.co.uk"
  ]
}' |
apify call pfistyle.digital/domain-whois-dns --silent --output-dataset

```

## MCP server setup

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

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/8s97qVuhPylL8ETqD/builds/JSK8aGb9y5jLfM6MM/openapi.json
