# Domain WHOIS & DNS Lookup: Registrar, Expiry, A/MX/NS (`ucchiii/domain-whois`) Actor

Bulk WHOIS (RDAP) and DNS lookup for a list of domains: registrar, created/updated/expiry dates, days to expiry, status, nameservers, A/MX/NS records. No personal data. Empty input = free demo. $0.50 per 1,000 domains.

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

## Pricing

from $0.35 / 1,000 domain looked ups

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 — bulk domain checker with registrar, expiry and DNS records

**For anyone who has to check many domains at once and is tired of pasting them one by one into a whois site:** domain investors and brokers, agencies and IT teams that manage client domains, security and brand-protection teams, and SEO people vetting expired or competitor domains. Give this Actor a list of domains (or a dataset) and get **one clean row per domain: registrar, created / updated / expiry dates, days to expiry, status, nameservers and A / MX / NS records**.
**$0.50 per 1,000 domains.** It uses **RDAP** (the modern replacement for port-43 WHOIS, from the IANA bootstrap) and **DNS over HTTPS** (Cloudflare or Google). No scraping, no proxies, no API keys.

### What it does

- **WHOIS data via RDAP**: registrar and its IANA ID, creation date, last-changed date, expiration date, **days to expiry**, `expired` flag, domain status codes (`client transfer prohibited`, …), nameservers, DNSSEC flag
- **DNS records via DNS over HTTPS**: A (IPv4), MX (with priority) and NS records, from Cloudflare (1.1.1.1) or Google Public DNS
- **Registered or not**: unregistered names come back as `not_registered` (and are free), so you can screen a list of names
- **Messy input is fine**: `https://WWW.Example.com/page?x=1` becomes `www.example.com`, internationalized names become punycode, duplicates are removed, typos come back as `invalid` rows (free)
- **TLDs without RDAP** (several country domains): the row is built from DNS and marked `dns_only`
- **Polite and robust**: per-server connection and spacing limits, timeouts, up to 3 retries with backoff (honours `Retry-After`), one failing domain never stops the run

### Privacy: no personal data

Registrant, administrative, technical and billing contacts are **never read or output** — no names, e-mails, phone numbers or addresses. Only the registrar (a company), dates, statuses and nameserver names are used. Every row carries `registrantRedacted: true`. E-mail addresses typed into the input are rejected as invalid, not looked up.
(Many registries redact registrant data anyway since GDPR; this Actor does not depend on that.)

### Input example

```json
{
  "domains": ["example.com", "python.org", "https://www.apify.com/store"],
  "includeDns": true,
  "dnsResolver": "cloudflare",
  "concurrency": 5
}
```

Or point it at a dataset of another Actor with `datasetId` and `domainField`.
**Empty input runs a free demo** on four domains (not charged, rows carry `_demo: true`).

### Output

One row per domain in the dataset (a table view for registration data and one for DNS records are included):

```json
{
  "domain": "example.com",
  "status": "ok",
  "registered": true,
  "registrar": "Example Registrar, Inc.",
  "registrarIanaId": "9999",
  "createdAt": "2001-01-01T00:00:00Z",
  "updatedAt": "2026-01-01T00:00:00Z",
  "expiresAt": "2027-01-01T00:00:00Z",
  "daysToExpiry": 100,
  "expired": false,
  "domainStatus": ["client transfer prohibited"],
  "nameservers": ["ns1.example.net", "ns2.example.net"],
  "dnssecSigned": false,
  "dnsA": ["192.0.2.1"],
  "dnsMx": ["10 mail.example.com"],
  "dnsNs": ["ns1.example.net", "ns2.example.net"],
  "rdapServer": "rdap.example-registry.test",
  "rdapDomain": "example.com",
  "registrantRedacted": true,
  "error": null,
  "checkedAt": "2026-10-21T09:00:00Z"
}
```

(Made-up placeholder values that show the shape of a row.) `status` is one of:
| status | meaning | charged |
|---|---|---|
| `ok` | RDAP registration data returned | yes |
| `dns_only` | the TLD has no RDAP service; DNS records returned | yes |
| `not_registered` | the registry says the name is not registered | no |
| `partial` | RDAP failed (see `error`) but DNS answered | no |
| `invalid` | not a valid domain name | no |
| `error` | the lookup failed (timeout, rate limit, server error; see `error`) | no |

The record `SUMMARY` in the key-value store counts each status and the number of charged domains.

### Use cases

- Watch a portfolio of client or brand domains for upcoming expiry (`daysToExpiry` below 30)
- Screen thousands of candidate names: which are registered, since when, with which registrar
- Find which domains still point to old nameservers or mail servers before a migration
- Enrich a list of domains from another scraper with registrar, age and DNS data

### Limits (cannot be raised by input)

- 50,000 domains per run; up to 10 in parallel; at most 2 connections to one server at a time, at least 0.2 s between requests to the same server
- Each request has a timeout (default 15 s, max 60 s) and is retried at most 3 times
- RDAP data comes from the registry or registrar of each TLD and can differ slightly between them (some omit the updated date, for example). Servers may answer with rate limits under heavy load; those domains come back as `error` and are not charged, so you can run them again
- Expiry dates are the registry's; a domain in a grace period can show a past date (`expired: true`)

### Pricing

Pay per event: **$0.0005 per domain looked up** ($0.50 per 1,000). Unregistered, invalid, partial and failed lookups are free. Set a maximum charge per run to cap spend; the Actor stops cleanly when it is reached.

### FAQ

**Is this the same as WHOIS?** It returns the same kind of registration data, through RDAP, the protocol ICANN requires registries and registrars to run. It does not connect to port-43 WHOIS servers.
**Why is there no owner name?** By design (see Privacy). The Actor is for domain status, expiry and DNS checks, not for finding people.
**Why `dns_only` for some domains?** A few country TLDs publish no RDAP service. You still get the DNS records and a registered flag.
**Can I check subdomains?** Yes. DNS records are those of the exact name you give. Registration data belongs to the registered domain, so for `www.example.com` the Actor looks up `example.com` and shows it in `rdapDomain`. (It has no public-suffix list: for names under multi-part suffixes such as `co.uk`, give the registered domain itself.)

### More tools from the same developer

- [Bulk URL Status Checker](https://apify.com/ucchiii/url-status-checker) — bulk HTTP status and redirect-chain checker for URL lists
- [Website Technology Detector](https://apify.com/ucchiii/website-technology-detector) — tech stack of websites in bulk: CMS, frameworks, analytics and tag IDs, CDN, consent tools
- [Sitemap URL Extractor](https://apify.com/ucchiii/sitemap-urls) — list every URL of a website from its sitemaps
- [Google Sheets Export](https://apify.com/ucchiii/google-sheets-export) — export an Apify dataset to Google Sheets
- [Dataset Merge & Dedup](https://apify.com/ucchiii/dataset-dedup-merge) — merge Apify datasets and remove duplicates by the fields you choose

# Actor input Schema

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

Domain names to look up, one per entry, e.g. 'example.com'. URLs ('https://www.example.com/page') are cut down to the host name; internationalized names (münchen.de) are converted to punycode. Duplicates are removed. Leave empty (and no dataset) to run a free demo (not charged). Up to 50,000 domains per run.

## `datasetId` (type: `string`):

Optional. Dataset ID (a finished run's 'defaultDatasetId', e.g. 'WkzbQMuFYuamGv3YF') whose items contain domain names or URLs, for example the output of another Actor. Added to the domains above.

## `domainField` (type: `string`):

Name of the field in each dataset item that holds the domain or URL. Default 'domain'.

## `includeDns` (type: `boolean`):

Also look up A, MX and NS records over DNS-over-HTTPS. Turn off for registration data only (faster). The price per domain is the same.

## `dnsResolver` (type: `string`):

Which public DNS-over-HTTPS JSON API answers the DNS questions. Registration data always comes from RDAP.

## `timeoutSecs` (type: `integer`):

Timeout for each request, in seconds. Failed requests are retried up to 3 times with a growing pause. Allowed range 1 to 60 (default 15).

## `concurrency` (type: `integer`):

How many domains are looked up in parallel. Allowed range 1 to 10 (default 5). At most 2 connections per server are used at a time, with at least 0.2 s between requests to the same server, whatever you set here.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "python.org",
    "apify.com"
  ],
  "domainField": "domain",
  "includeDns": true,
  "dnsResolver": "cloudflare",
  "timeoutSecs": 15,
  "concurrency": 5
}
```

# Actor output Schema

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

One row per domain in the default dataset: registrar, dates, days to expiry, status, nameservers and DNS records. Download as CSV, JSON, Excel and more from here.

## `summary` (type: `string`):

Record SUMMARY: how many domains were found, not registered, invalid, partial or failed, and how many were charged.

# 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": [
        "example.com",
        "python.org",
        "apify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ucchiii/domain-whois").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": [
        "example.com",
        "python.org",
        "apify.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("ucchiii/domain-whois").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": [
    "example.com",
    "python.org",
    "apify.com"
  ]
}' |
apify call ucchiii/domain-whois --silent --output-dataset

```

## MCP server setup

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

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/74db2hQYmsmd9777D/builds/rHYrRd2xg4PKVwxGn/openapi.json
