# Bulk WHOIS & Domain Age Lookup (RDAP) - Expiry, Registrar (`lukehunter/domain-whois-rdap-lookup`) Actor

Bulk WHOIS lookup over RDAP, the official successor protocol: domain age checker, domain expiry date, registrar, nameservers, DNSSEC and availability for domains, URLs, emails or Google Maps rows. No personal data. $0.003 per registered domain; available domains and failures are free.

- **URL**: https://apify.com/lukehunter/domain-whois-rdap-lookup.md
- **Developed by:** [Luke Hunter](https://apify.com/lukehunter) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 registered 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.

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

## Bulk WHOIS & Domain Age Lookup (RDAP) - Expiry, Registrar

**For sales, SEO, security, domain-investing and data teams that need WHOIS data for a whole list of domains.** Paste domains, URLs, emails or a Google Maps / lead dataset and get one clean row per domain: the **registration date and domain age in days**, the **domain expiry date and days to expiry**, the **registrar** (name, IANA ID, website), **nameservers**, **DNSSEC**, status codes and, where the registry publishes it, the **registrant organisation and country**. It is a **bulk WHOIS lookup** and **domain age checker** built on **RDAP**, the official IETF replacement for WHOIS (RFC 9082 and 9083), so the answers are structured JSON from the registries themselves, not parsed WHOIS text.

**$0.003 per registered domain with data. Available (unregistered) domains, TLDs without RDAP and failed lookups are free. 1,000 domains = $3.**

No personal contact data, ever: emails, phone numbers, street addresses and the names of private individuals are dropped before anything is saved. Only the registrant's organisation name and country are kept, and only when the registry or registrar publishes them.

### What you get

| Field | Meaning |
|---|---|
| `domain`, `input`, `tld` | The registrable domain looked up (www, paths and subdomains removed, IDN as punycode), your original entry, and its TLD. |
| `status` | `registered` (charged), or free: `available`, `no-rdap`, `invalid`, `skipped`, `blocked`, `rate-limited`, `error`, `not-processed`. |
| `registered` | `true`, `false` (the registry has no record: available) or `null` (no answer). |
| `registrationDate`, `domainAgeDays` | When the domain was first registered, and its age in whole days at the check. |
| `expirationDate`, `daysToExpiry` | When the registration expires, and days left (negative if already past). |
| `lastChangedDate` | When the record last changed. |
| `statusCodes` | RDAP status values, e.g. `client transfer prohibited`, `server delete prohibited`. |
| `registrar`, `registrarIanaId`, `registrarUrl` | The sponsoring registrar. |
| `nameservers`, `dnssec` | Delegated nameservers and whether the delegation is DNSSEC-signed. |
| `registrantOrganization`, `registrantCountry` | Only when published. Many registries redact them, so expect `null` for most domains. |
| `rdapServer`, `registrarRdapServer` | The registry RDAP server that answered, and the registrar's RDAP server when it was also read. |
| `warnings`, `retryable`, `message`, `checkedAt` | Data-quality notes, whether running again later may help, why a row has no data, and the check time. |

### Quick start

```json
{
  "domains": ["apify.com", "wikipedia.org", "github.io"],
  "maxItems": 5
}
```

Entries can be domains (`apify.com`), URLs (`https://www.apify.com/pricing`), emails (`sales@apify.com`) or non-ASCII names (`bücher.de`). Subdomains are reduced to the registrable domain (`blog.example.co.uk` becomes `example.co.uk`) using the public suffix list, and duplicates are merged. Addresses at free-mail providers (gmail.com and similar) are skipped when they come from an email.

### How it works

1. The Actor loads the **IANA RDAP bootstrap file** (`data.iana.org/rdap/dns.json`) once per run. It lists the authoritative RDAP server of every TLD that has one.
2. For each domain it asks that registry: `GET {server}domain/{domain}` with `Accept: application/rdap+json`.
3. If the registry answer links to the **registrar's own RDAP server** (the usual case for `.com` and `.net`), it follows that link once to add registrar-level data such as the registrant organisation, when the registrar publishes it.
4. An RDAP **404** means the registry has no record: the domain is reported as `available` and costs nothing.

It is deliberately gentle: `robots.txt` of every RDAP host is read and obeyed, requests to one host are spaced at least one second apart (longer if `Crawl-delay` asks), at most five domains are in flight at a time, and a `429 Too Many Requests` is honoured with `Retry-After` and backoff. It never tries to get around a refusal.

### Recipes

**Domain age for lead scoring.** Brand-new domains are a common sign of throwaway or low-trust companies; old domains correlate with established ones. Run your lead list and filter on `domainAgeDays`:

```json
{
  "domains": ["acme.com", "https://www.example-startup.io/about", "jane@example-shop.co.uk"],
  "maxItems": 500
}
```

Sort or filter the dataset on `domainAgeDays` (for example under 90 days is "new") and keep `registrar` and `nameservers` for context.

**Expiring domains watch.** Save your domain list as a Task, add a Schedule (weekly is plenty) and send the results to a Google Sheet, Slack or a webhook through the Integrations tab. Alert on `daysToExpiry` below 30:

```json
{
  "domains": ["mycompany.com", "mycompany.org", "mycompany-shop.com"],
  "maxItems": 100
}
```

Use it for your own portfolio, for suppliers and partners, or for competitors. A row with `daysToExpiry` below 0 means the registration has lapsed but the registry still holds the name (a grace period).

**Enrich a Google Maps lead list with domain age.** Run a Google Maps scraper first, then pass its default dataset ID. The Actor reads the `website` (or `domain`, `url`, `email`) field of each row:

```json
{
  "datasetId": "<DEFAULT_DATASET_ID_OF_THE_GOOGLE_MAPS_RUN>",
  "maxItems": 1000
}
```

Join the result back to your leads on `input` (the website exactly as it appeared). The same works with datasets from [Website Contact Scraper](https://apify.com/lukehunter/company-contact-scraper) and [Google Maps Email Extractor & Lead Enricher](https://apify.com/lukehunter/lead-enricher). Or pass rows directly: `"rows": [{"title": "Acme", "website": "https://acme.com"}]`.

**Find available domains.** Check a list of candidate names; rows with `status: "available"` are unregistered according to the registry and are free.

### Input

| Field | Default | What it does |
|---|---:|---|
| `domains` | none | Domains, URLs or emails. |
| `rows` | none | Objects with a `website`, `domain`, `url` or `email` field, merged with `domains`. |
| `datasetId` | none | An Apify dataset of such rows (up to 50,000 rows are read). |
| `maxItems` | `100` | Hard cap on domains looked up and charged, 1 to 10,000. Domains beyond it are not looked up. |

### Example output

```json
{
  "domain": "acme-widgets.com",
  "input": "https://www.acme-widgets.com/about",
  "tld": "com",
  "status": "registered",
  "registered": true,
  "statusCodes": ["client delete prohibited", "client transfer prohibited", "client update prohibited", "server delete prohibited"],
  "registrationDate": "2015-03-12T09:30:00.000Z",
  "expirationDate": "2027-03-12T09:30:00.000Z",
  "lastChangedDate": "2026-02-01T11:00:00.000Z",
  "domainAgeDays": 4232,
  "daysToExpiry": 150,
  "registrar": "Example Registrar, Inc.",
  "registrarIanaId": 9999,
  "registrarUrl": "https://www.registrar.example",
  "nameservers": ["ns1.dns-host.example", "ns2.dns-host.example"],
  "dnssec": false,
  "registrantOrganization": "Acme Widgets Ltd",
  "registrantCountry": "GB",
  "rdapServer": "https://rdap.registry.example/com/v1/",
  "registrarRdapServer": "https://rdap.registrar.example/",
  "warnings": [],
  "retryable": false,
  "message": null,
  "checkedAt": "2026-10-12T12:00:00.000Z"
}
```

The sample is illustrative (an invented company on invented servers), shaped like real RDAP answers. An unregistered domain looks like this and is free:

```json
{
  "domain": "free-name-1234.com",
  "input": "free-name-1234.com",
  "tld": "com",
  "status": "available",
  "registered": false,
  "statusCodes": [],
  "registrationDate": null,
  "expirationDate": null,
  "domainAgeDays": null,
  "daysToExpiry": null,
  "registrar": null,
  "nameservers": [],
  "rdapServer": "https://rdap.registry.example/com/v1/",
  "retryable": false,
  "message": "The .com registry has no record of free-name-1234.com (RDAP 404): it looks unregistered and available. Free."
}
```

(Fields with no data are `null` or empty in every row; the sample is shortened here.) Status meanings:

- `registered`: RDAP data found. **Charged.**
- `available`: the registry has no record (RDAP 404). Free.
- `no-rdap`: the TLD has no RDAP server in the IANA bootstrap file, so there is nothing to ask. Free, with a clear message.
- `invalid` / `skipped`: not a domain, or a free-mail address. Free.
- `blocked`: the host's `robots.txt` disallows the request or could not be read, or the server refused it. Free. `retryable` says whether trying later may help.
- `rate-limited` / `error`: the server kept saying 429, had an error or could not be reached. Free and `retryable`.
- `not-processed`: not looked up because your spending limit or the run time limit was reached. Free; run those again.

### Use with AI agents (MCP)

This Actor works as a tool for Claude, ChatGPT, Cursor and other MCP clients through [Apify's MCP server](https://mcp.apify.com). Every input property description is short and carries an example, so agents fill it in correctly. None is required, but give it `domains`, `rows` or a `datasetId`.

**Claude Desktop** (`claude_desktop_config.json`), with your own token from Apify Console > Settings > API & Integrations:

```json
{
  "mcpServers": {
    "apify-domain-whois-rdap-lookup": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://mcp.apify.com?tools=lukehunter/domain-whois-rdap-lookup",
        "--header", "Authorization: Bearer ${APIFY_TOKEN}"
      ],
      "env": { "APIFY_TOKEN": "<YOUR_APIFY_TOKEN>" }
    }
  }
}
```

Clients that support OAuth (Claude.ai, VS Code) can connect with just the URL `https://mcp.apify.com?tools=lukehunter/domain-whois-rdap-lookup` and sign in to Apify. Never paste a real token into a shared file or chat.

**Example.** You ask: "How old is apify.com and when does it expire?" The agent calls `lukehunter--domain-whois-rdap-lookup` with:

```json
{
  "domains": ["apify.com"],
  "maxItems": 1
}
```

It is charged $0.003 for the one registered domain, capped by `maxItems`.

### Pricing

Pay-per-event: **$0.003 per registered domain** (event `domain`), meaning one domain whose RDAP data was read. Available domains, TLDs without RDAP, blocked, rate-limited and failed lookups and `not-processed` rows are free.

| Domains | Cost if all are registered |
|---:|---:|
| 5 | $0.015 |
| 100 (default cap) | $0.30 |
| 1,000 | $3.00 |
| 10,000 (maximum) | $30.00 |

For comparison, other WHOIS and domain lookup Actors on the Apify Store were listed in October 2026 at roughly $0.0025 to $0.003 per domain, with one outlier at $0.02, and a free availability checker without a paid path. `maxItems` and your Apify per-run spending limit both stop delivery before the cap is passed: a result is delivered and charged together, and never past your limit. Price changes follow Apify's notice rules; the Pricing tab always shows the rate you are charged.

### Limitations

- **RDAP availability varies by TLD.** Most generic TLDs (`.com`, `.net`, `.org`, `.io` and hundreds of new ones) have RDAP servers, but some country-code registries only offer WHOIS or a web form, and a few publish very little. Those come back as `no-rdap` (free). The server list comes from the IANA bootstrap file at run time, so it is always current.
- **Registrant data is often redacted.** Since GDPR, most registries and registrars hide registrant details. `registrantOrganization` and `registrantCountry` are `null` unless they are really published. This Actor does not try to unmask anything.
- **No personal contact data by design.** Emails, phones, street addresses and the names of private individuals are never read into the output, even when a server publishes them. If you need contact data, use the registrar's own channels.
- **Registry limits and speed.** Registries throttle bulk use, so the Actor sends one request per second per host (two per `.com` domain when the registrar is also read). 1,000 `.com` domains take roughly 30 to 35 minutes. A run stops after 5 hours and reports what is left. Different TLDs are looked up in parallel.
- **`robots.txt` is obeyed.** If an RDAP host disallows the request, or its `robots.txt` cannot be read (server error, refusal), that host is not requested in the run and its domains come back as `blocked` (free). A host that answers 404 or another 4xx for `robots.txt` has no restrictions, as RFC 9309 says.
- **It reports the registration, not the owner's intent.** `available` means the registry has no record right now. Reserved and premium names, and names a registry blocks, can also answer 404. Confirm with a registrar before buying.
- **Dates are the registry's.** Expiry dates move when a domain is renewed; schedule the Actor if you need to track them. A negative `daysToExpiry` means the registration has lapsed but the name is still held by the registry.
- **Not legal advice.** This Actor is independent and not affiliated with ICANN, IANA or any registry or registrar. RDAP data is public registration data; use it in line with each registry's terms and applicable law.

### Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

**One API call that runs the Actor and returns the data** (replace `$APIFY_TOKEN` with your token from Apify Console → Settings → Integrations):

```bash
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~domain-whois-rdap-lookup/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"domains": ["apify.com", "wikipedia.org", "github.io"], "maxItems": 5}'
```

**JavaScript** (`npm i apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~domain-whois-rdap-lookup').call({
  "domains": [
    "apify.com",
    "wikipedia.org",
    "github.io"
  ],
  "maxItems": 5
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~domain-whois-rdap-lookup').call(run_input={"domains": ["apify.com", "wikipedia.org", "github.io"], "maxItems": 5})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

**Schedules, webhooks and no-code tools:** save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

**Feedback:** an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

### FAQ

**What is RDAP and why not WHOIS?** RDAP (Registration Data Access Protocol) is the IETF standard that replaces the port-43 WHOIS protocol. It returns structured JSON over HTTPS, with standard fields for dates, status and nameservers, and ICANN requires it of generic TLD registries and registrars. That makes the data cleaner and the lookups more reliable than parsing free-text WHOIS.

**Why is the registrant empty for most domains?** Registries and registrars redact it for privacy. The Actor outputs an organisation and country only when one is genuinely published.

**Which domains are charged?** Only `registered` rows, $0.003 each. `available`, `no-rdap`, `blocked`, `rate-limited`, `error`, `invalid`, `skipped` and `not-processed` are free.

**Does it check subdomains?** No. WHOIS is about the registered domain, so `blog.example.co.uk` is looked up as `example.co.uk`. For `github.io`, a public suffix owned by one registrant, the domain itself is looked up.

**How is `domainAgeDays` calculated?** Whole days from the RDAP registration event to the moment of the check. It is `null` when the registry publishes no registration date.

**A lookup came back `rate-limited`. What now?** The registry kept asking to slow down. Run those domains again later; they were not charged.

### Related Actors

Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.

- **[Domain Email Provider Lookup](https://apify.com/lukehunter/email-provider-mx-lookup)**: the email provider (Google Workspace, Microsoft 365, Zoho and more), MX, SPF and DMARC setup of any domain list, with provider-change alerts.
- **[French Company Leads](https://apify.com/lukehunter/french-companies-sirene)**: French companies (entreprises) from the official Sirene-based API, filtered by NAF code, department, headcount and creation date, with SIREN/SIRET, officers and new-company alerts.
- **[Poland KRS Company Lookup](https://apify.com/lukehunter/poland-krs-company-lookup)**: Polish companies from the official KRS API by KRS number: NIP, REGON, address, share capital, PKD and the board as published, with optional VAT status.
- **[EU VAT Number Validator](https://apify.com/lukehunter/eu-vat-number-validator)**: bulk EU VAT number checks against the official VIES service: valid or invalid, registered name and address, with optional consultation numbers.
- **[Website Contact Scraper](https://apify.com/lukehunter/company-contact-scraper)**: emails, phone numbers and social profile links from any list of company domains, with the page each came from and a changed-contacts-only mode for scheduled runs.
- **[Impressum Scraper](https://apify.com/lukehunter/impressum-scraper)**: legal entity data from the imprint of any company website in Germany, Austria and Switzerland: legal name and form, managing directors, HRB/HRA or FN register, VAT id and address, with a changed-data-only mode for scheduled runs.
- **[Website Tech Stack Detector](https://apify.com/lukehunter/website-tech-stack-detector)**: the technologies a website runs (CMS, ecommerce platform, analytics, ad pixels, chat, payments, CDN, frameworks) from its home page, with the evidence for each match and a changed-technologies-only mode for scheduled runs.
- **[Google Maps Email Extractor & Lead Enricher](https://apify.com/lukehunter/lead-enricher)**: emails, phones, social links, named people and the tech stack of every business in your Google Maps Scraper results, one enriched row per business, with filters such as only Shopify stores with an email.

# Actor input Schema

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

Domains to look up. Also accepts URLs and email addresses; scheme, www, paths and the part before @ are stripped, subdomains are reduced to the registrable domain, and non-ASCII names become punycode. Example: \["apify.com", "https://www.wikipedia.org/wiki/X", "sales@github.io"]. Duplicates are merged.

## `rows` (type: `array`):

Rows from a Google Maps or lead dataset: objects with a website, domain, url or email field. The first usable one per row is used. Example: \[{"title": "Acme", "website": "https://acme.com"}, {"email": "info@example.org"}]. Free-mail addresses such as gmail.com are skipped.

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

Read rows from an Apify dataset in your account instead of pasting them, for example the default dataset ID of a Google Maps scraper run, e.g. Wa5gtYQ2FkBpHnBMs. Rows need a website, domain or email field. Up to 50,000 rows are read; maxItems still applies.

## `maxItems` (type: `integer`):

Hard cap on domains looked up and charged, 1 to 10000, e.g. 100. At $0.003 per registered domain, 100 domains cost at most $0.30. Domains beyond the cap are not looked up or charged. Available domains, TLDs without RDAP and failed lookups are never charged.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "wikipedia.org",
    "github.io"
  ],
  "maxItems": 5
}
```

# Actor output Schema

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

No description

# 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",
        "wikipedia.org",
        "github.io"
    ],
    "maxItems": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukehunter/domain-whois-rdap-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": [
        "apify.com",
        "wikipedia.org",
        "github.io",
    ],
    "maxItems": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("lukehunter/domain-whois-rdap-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": [
    "apify.com",
    "wikipedia.org",
    "github.io"
  ],
  "maxItems": 5
}' |
apify call lukehunter/domain-whois-rdap-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lukehunter/domain-whois-rdap-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/idHc2pzuv0Lm4cR1O/builds/ojh8Oh7uBfRhar4pK/openapi.json
