# WHOIS Lookup & Domain Age Checker: Bulk Expiry, RDAP from CSV (`nerolabs/domain-whois-checker`) Actor

Registration date, expiry, domain age, registrar and nameservers for every domain in an Apify dataset, CSV or Google Sheet, from the registries' own RDAP (WHOIS) servers. Keeps every original column. Inputs: datasetId or fileUrl, domainField. Charged per registry answer. Agent-ready: x402, MCP.

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

## Pricing

from $3.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.
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?

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

## Domain WHOIS & Age Checker (Dataset, CSV or Sheet)

**How old is each of these companies, when does their domain expire, and who is it registered with?**

Looking that up one domain at a time is a browser tab per company. This takes the list you already have, a dataset, a CSV, an Excel file or a Google Sheet, and adds the registry's own answer to every row: when the domain was registered, when it expires, how old it is, the registrar, the nameservers and whether it is locked.

Every original column comes back untouched, so nothing has to be matched up by hand afterwards.

### Why the registration date is worth having

A domain registered four months ago is a brand new business. One registered in 1998 is not. That single column changes how a list is worked:

- **Lead qualification.** Sort a scraped lead list by domain age and you have sorted it by roughly how established each business is.
- **Fraud and trust checks.** A supplier, a marketplace seller or an invoice from a domain registered last week is worth a second look.
- **Portfolio and client admin.** Which of the domains you or your clients own expire in the next 30 days, and which are not transfer-locked.
- **Acquisition.** Which names on your shortlist are not registered at all, and when the taken ones come up for renewal.

### Where the data comes from

From the registry that actually holds the record, over **RDAP**, the registries' own modern replacement for WHOIS. No scraping, no third-party WHOIS reseller, no API key. The list of which registry serves which top-level domain is IANA's own, read fresh on every run, so a newly delegated TLD works the day it is delegated.

**What this means in practice, stated up front rather than in the small print:**

- **Some country registries run no RDAP server at all.** `.ie` and `.de` are the best-known. Those rows come back as `no_rdap_server` with empty fields, **and they are not charged**. Nothing is guessed from another source.
- **Registrant contact details are redacted by most registries** since GDPR. This Actor returns only an organisation name and a country code where a registry publishes them, and only if you switch that on. It never returns a person's name, email, phone or address.
- **A date the registry does not publish comes back empty, not estimated.** A blank is more useful than a confident wrong number.
- **The expiry date is the registry expiry date.** It is when the registration lapses if nobody renews, not the date a website goes dark.

### What you get on every row

| Column | What it is |
|---|---|
| `domain` | The registrable domain read out of your column. `https://www.bbc.co.uk/news` becomes `bbc.co.uk` |
| `domainStatus` | `ok`, `not_registered`, `no_rdap_server`, `rate_limited`, `refused`, `unreachable` or `invalid_domain` |
| `registeredOn`, `expiresOn`, `lastChangedOn` | The registry's own dates |
| `domainAgeDays`, `domainAgeYears` | How long the name has been registered |
| `daysUntilExpiry`, `isExpired`, `expiringSoon` | Counted in calendar days to the expiry date |
| `isNewDomain` | Registered inside your "recently registered" window, 365 days by default |
| `registrar`, `registrarIanaId` | Who the name is registered through |
| `nameservers`, `nameserverCount` | Which nameservers it uses, so you can see the DNS or hosting provider |
| `domainLocked`, `pendingDelete`, `domainStatuses` | Transfer lock, pending deletion, and the raw registry statuses |
| `dnssec` | Whether the domain is DNSSEC signed |
| `rdapServer`, `checkedAt` | Which registry answered, and when |

### Example

Four rows in:

| company | domain |
|---|---|
| Anthropic | anthropic.com |
| BBC | https://www.bbc.co.uk/news |
| Gymshark | gymshark.com |
| Linear | linear.app |

and back (real output, shortened):

| company | domain | registeredOn | expiresOn | domainAgeYears | registrar | nameservers |
|---|---|---|---|---|---|---|
| Anthropic | anthropic.com | 2001-10-02 | 2033-10-02 | 24.9 | MarkMonitor Inc. | isla.ns.cloudflare.com, randy.ns.cloudflare.com |
| BBC | bbc.co.uk | 1994-12-13 | 2034-12-13 | 31.7 | British Broadcasting Corporation | ddns0.bbc.co.uk, ddns0.bbc.com, dns0.bbc.co.uk, … |
| Gymshark | gymshark.com | 2011-09-16 | 2028-09-16 | 15 | MarkMonitor Inc. | ns-1444.awsdns-52.org, ns-294.awsdns-36.com, … |
| Linear | linear.app | 2018-05-09 | 2030-05-09 | 8.3 | CloudFlare, Inc. | tim.ns.cloudflare.com, zara.ns.cloudflare.com |

Worth noticing in that one table: Anthropic's name is registered through MarkMonitor but its DNS is served by Cloudflare, and the BBC is its own registrar. Both are facts the nameserver column gives you for free.

### Input

Point it at whichever you have:

- **Dataset**: pick an Apify dataset, for example the output of a Google Maps, directory or company scraper.
- **File URL**: a public link to a CSV, TSV, Excel, JSON or JSON Lines file, or a Google Sheet shared as "anyone with the link".
- **Inline data**: paste rows as JSON.

The domain column is detected automatically. Full URLs, bare domains and even email addresses all work.

Then narrow the results if you want: keep only registered domains, only unregistered ones, only those expiring soon, only those registered recently, or only the rows the registry would not answer for.

### Output

The dataset, plus optionally a real downloadable **CSV or Excel file**, plus optionally an append to a **named dataset** so a scheduled run builds one growing table, plus optionally a **webhook** that receives the run summary the moment the run finishes.

### What it costs

**$0.005 per domain the registry answers for**, which is $5.00 per 1,000 domains at the standard rate and less on Bronze, Silver and Gold. Files are $0.01 each, a webhook delivery is $0.02.

**You are charged for an answer, not for an attempt.** A real record is an answer. So is a registry confirming a name is not registered, which is exactly the answer you want when you are checking availability. A TLD with no RDAP server, a refusal, a rate limit, a timeout and a value that is not a domain are all **free**.

Filtering happens after the lookup, so choosing to keep only the expiring rows does not make the run cheaper.

A 1,000-domain list costs about **$5**, and finishes in a couple of minutes.

### FAQ

**Is this legal?** Yes. RDAP is a published standard (RFC 9082 and 9083) that registries run precisely so that domain registration data can be queried programmatically. There is no scraping here and no terms to work around.

**Why is `.de` or `.ie` empty?** Those registries have not deployed RDAP. Rather than fall back to a scraped or resold WHOIS source of unknown accuracy, this Actor says so plainly and does not charge you.

**Why is the registrant blank?** Because the registry redacts it, which has been the norm since GDPR. Where a registry does publish an organisation, switch on "Include registrant organisation and country" to get it.

**Can it tell me if a domain is for sale?** It tells you whether the name is registered, when it expires and whether it is locked. A parked or for-sale page is a website question, not a registry one.

**I am seeing `rate_limited`.** Registries limit how fast one client may query. Lower the Concurrency setting, or split a very large list across a few runs.

**Does it run JavaScript or visit the website?** No. It never touches the website at all, only the registry. That is why it is fast and cheap.

### The rest of the toolkit

The natural pair is **[Tech Stack Detector](https://apify.com/nerolabs/tech-stack-detector)** and **[Website Contact Finder](https://apify.com/nerolabs/website-contact-finder)**: run all three on the same list, or chain them with the Pipeline Runner, to get every company's age, its tech stack and its business inbox in one table. Then [Email List Cleaner & Validator](https://apify.com/nerolabs/email-list-cleaner) checks the addresses and [Phone Number Validator & Cleaner](https://apify.com/nerolabs/phone-number-validator) checks the numbers.

For the data itself: [Dataset Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter), [Filter & Transform](https://apify.com/nerolabs/dataset-filter-transform), [Join & Merge](https://apify.com/nerolabs/dataset-join-merge), [Aggregate, Group By & Pivot](https://apify.com/nerolabs/dataset-aggregate-pivot), [Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector), [AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich), [Charts & Report](https://apify.com/nerolabs/dataset-charts-report), [to Postgres, Supabase & MySQL](https://apify.com/nerolabs/dataset-to-database), [to REST API](https://apify.com/nerolabs/dataset-to-rest-api), and [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner) to chain them in one call.

***

If this saved you looking up a few hundred domains one at a time, a review on the Store page helps a lot. If something looks wrong, open an issue on the Issues tab and I will answer personally.

# Actor input Schema

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

An Apify dataset whose rows each hold a domain or website. Use the picker so the run is allowed to read it. Leave empty to use a file URL or inline data instead.

## `fileUrl` (type: `string`):

A public link to a CSV, TSV, Excel (.xlsx), JSON or JSON Lines file, or a Google Sheet shared as 'anyone with the link'. Used when no dataset is set.

## `data` (type: `array`):

Rows as a JSON array, each with a domain or website field. Used when neither a dataset nor a file URL is set.

## `fileFormat` (type: `string`):

How to read the file URL. 'Detect automatically' works from the extension, the content type and the first bytes.

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

The column holding each domain, for example 'domain' or 'website'. Leave empty to detect it automatically. Full URLs, bare domains and even an email address all work: 'https://www.bbc.co.uk/news' is read as 'bbc.co.uk'.

## `keep` (type: `string`):

Which rows end up in the results. Filtering happens after the lookup, so it does not change what a run costs.

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

Sets the 'expiringSoon' column and the 'Expiring soon only' filter.

## `newerThanDays` (type: `integer`):

Sets the 'isNewDomain' column and the 'Recently registered only' filter. A domain registered a few months ago usually means a brand new business.

## `includeRegistrantOrganization` (type: `boolean`):

Off by default. Most registries redact registrant details, and where they do publish something this Actor returns only an organisation name and a country code, never a person's name, email, phone or address.

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

How many domains are looked up at once. Requests to any one registry are spaced out regardless, because registries rate-limit. Lower this if you see 'rate\_limited' rows.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for one registry response before giving up on it.

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

A safety cap on how many rows are read from the input. Leave empty for no cap (up to 50,000).

## `exportFormats` (type: `array`):

Optionally write the results as a real downloadable CSV and/or Excel file as well as the dataset.

## `outputDatasetName` (type: `string`):

Optional. The name of a dataset to append every run's results to, so a scheduled run builds one growing table instead of a new dataset each time.

## `webhookUrl` (type: `string`):

Optional. When the run finishes, the summary (counts, top registrars, download links) is POSTed here as JSON, so a scheduled expiry watch can report into Slack, Zapier, Make, n8n or your own API.

## Actor input object example

```json
{
  "data": [
    {
      "company": "Anthropic",
      "domain": "anthropic.com"
    },
    {
      "company": "BBC",
      "domain": "https://www.bbc.co.uk/news"
    },
    {
      "company": "Gymshark",
      "domain": "gymshark.com"
    },
    {
      "company": "Linear",
      "domain": "linear.app"
    }
  ],
  "fileFormat": "auto",
  "keep": "all",
  "expiringWithinDays": 30,
  "newerThanDays": 365,
  "includeRegistrantOrganization": false,
  "concurrency": 5,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

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

Every original row with its registration date, expiry, age, registrar and status added.

## `domainSummary` (type: `string`):

Statuses, top registrars, expiring and recently registered counts, and the data note.

# 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 = {
    "data": [
        {
            "company": "Anthropic",
            "domain": "anthropic.com"
        },
        {
            "company": "BBC",
            "domain": "https://www.bbc.co.uk/news"
        },
        {
            "company": "Gymshark",
            "domain": "gymshark.com"
        },
        {
            "company": "Linear",
            "domain": "linear.app"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/domain-whois-checker").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 = { "data": [
        {
            "company": "Anthropic",
            "domain": "anthropic.com",
        },
        {
            "company": "BBC",
            "domain": "https://www.bbc.co.uk/news",
        },
        {
            "company": "Gymshark",
            "domain": "gymshark.com",
        },
        {
            "company": "Linear",
            "domain": "linear.app",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/domain-whois-checker").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 '{
  "data": [
    {
      "company": "Anthropic",
      "domain": "anthropic.com"
    },
    {
      "company": "BBC",
      "domain": "https://www.bbc.co.uk/news"
    },
    {
      "company": "Gymshark",
      "domain": "gymshark.com"
    },
    {
      "company": "Linear",
      "domain": "linear.app"
    }
  ]
}' |
apify call nerolabs/domain-whois-checker --silent --output-dataset

```

## MCP server setup

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

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/yFgbIiMs3UpeFcAJg/builds/NFcDOK5LvZvON85NL/openapi.json
