# WHOIS & RDAP Domain Expiry Monitor (`codeclouds/whois-rdap-domain-expiry-monitor`) Actor

Bulk WHOIS/RDAP lookup and domain expiry monitor for 1,200+ TLDs. Get registrar, owner organisation, creation and expiry date, days left, EPP status, transfer lock, nameservers, DNS provider and DNSSEC from official registries. Alerts on expiring, dropped or changed domains; checks availability.

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

## Pricing

from $2.00 / 1,000 domain results

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

## WHOIS & RDAP Domain Expiry Monitor

Bulk WHOIS lookup, domain expiry monitoring and availability checks, straight from the official registries
of 1,200+ TLDs. Give it a list of domains (or URLs, or e-mail addresses) and get back, per domain:

- **who owns it**: registrant organisation and country, or whether it is hidden by GDPR redaction or a
  privacy/proxy service;
- **when it expires**: expiry date, **days until expiry**, auto-renew grace and pending-delete status;
- **how it is set up**: registrar, EPP status codes, transfer lock, nameservers, **DNS provider**
  (Cloudflare, Route 53, ...), DNSSEC and domain age;
- **what changed** since the last run: renewals, registrar or owner changes, nameserver switches, a
  removed transfer lock, and domains that dropped or were just registered.

The data comes from **RDAP**, the official JSON successor of WHOIS that ICANN requires for all generic
TLDs. For registries that keep owner data at the registrar (.com, .net and most gTLDs), the Actor follows
the registry's link to the **registrar's own RDAP server**, which is where the owner organisation lives.
Country-code TLDs without RDAP (.de, .eu, .io, .co, .it, .se, .dk, .jp and many more), or whose RDAP server
is down, are answered through that registry's official port-43 WHOIS, with a parser tested on 30+
registry formats. It uses no scraping, no proxies and no third-party WHOIS resellers: about 60 domains in
25 seconds.

### When should an AI agent use this?

Use this Actor when a question needs **authoritative domain registration data** for one or many domains:

- "When does acme.com expire, and who is the registrar?"
- "Who owns stripe.io? Is the owner hidden behind a privacy service?"
- "Which of these 200 company domains expire in the next 60 days?"
- "Is 'lumora' still free as .com, .io or .ai?" (use `names` + `tlds`)
- "Did any of our domains change registrar, owner or nameservers since last week?" (scheduled, `trackChanges`)
- "Was this domain registered less than 30 days ago, and which registrar abuse address handles it?"

It returns one structured JSON record per domain, so an agent can answer directly from fields such as
`expiresAt`, `daysUntilExpiry`, `registrar`, `registrantOrganization`, `isRegistered`, `domainAgeDays`,
`dnsProvider` and `riskFlags`. It does not return DNS records, website content or traffic estimates.

### What this Actor does

- **Accepts messy input.** `https://www.bbc.co.uk/news` is checked as `bbc.co.uk`, `info@acme.io` as
  `acme.io`, `bücher.de` as its punycode form (`domainUnicode` keeps the readable name). Lists such as
  `"a.com, b.io; c.nl"` in one entry are split. Duplicates are checked once. The Actor knows the Public
  Suffix List, so `co.uk`, `com.au` and similar suffixes are handled correctly.
- **Finds the right registry** through the IANA RDAP bootstrap registry and queries the authoritative
  server directly: Verisign for .com/.net, PIR for .org, Nominet for .uk, SIDN for .nl, AFNIC for .fr,
  CIRA for .ca, Google Registry for .app/.dev, and so on.
- **Finds the owner** at the registrar's RDAP server for gTLDs (for example google.com → Google LLC,
  US; booking.com → Booking.com B.V., NL). It tells apart `public`, `redacted` (GDPR) and `proxy`
  (Domains By Proxy, Withheld for Privacy, ...).
- **Falls back to WHOIS** for ccTLDs without RDAP, and for registries whose RDAP server is unreachable. An
  unreachable server is detected once per run and skipped from then on, so it does not slow the run down.
- **Computes what you actually need:** `daysUntilExpiry`, `expiryStatus` (`ok`, `expiring_soon`,
  `expired`, `unknown`), `domainAgeDays`, `transferLocked`, `onHold`, `pendingDelete`, `dnsProvider`.
- **Raises risk flags:** `expiring_soon`, `expired`, `auto_renew_grace`, `pending_delete`,
  `transfer_pending`, `on_hold`, `no_transfer_lock`, `dnssec_unsigned`, `no_nameservers`, `parked`,
  `newly_registered`. `no_transfer_lock` is only raised where the registry supports lock statuses (all gTLDs, and ccTLDs that use them); ccTLDs such as .it, .dk or .nl protect transfers with auth codes, so there `transferLocked` stays `null`. On scheduled runs it adds `renewed`, `registrar_changed`, `owner_changed`,
  `nameservers_changed`, `status_changed`, `transfer_lock_removed`, `dropped` and `newly_taken`.
- **Keeps change alerts meaningful.** Short grace-period statuses after a normal renewal, "MarkMonitor
  Inc." versus "MarkMonitor, Inc.", or an owner lookup that failed once never trigger a false alert. A
  dropped domain reports one change (dropped), not a list of fields that became empty.
- **Three output modes.** `all`, `alerts_only` (you only get, and pay for, domains that need attention)
  and `available_only` (for availability sweeps). Errors, unsupported TLDs and invalid entries are always included, and free, so a monitoring run never silently skips a domain. The
  `SUMMARY` record always holds the complete overview.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `domains` | array of strings | – | Domains, subdomains, URLs or e-mail addresses. |
| `names` | array of strings | – | Brand names to check across `tlds` (availability sweep). |
| `tlds` | array of strings | com, net, org, io, ai, co, app, dev | TLDs used for `names`. |
| `expiryWarningDays` | integer | 30 | Domains that expire within this many days get `expiryStatus: expiring_soon`. |
| `outputFilter` | `all` / `alerts_only` / `available_only` | `all` | Which results to deliver. |
| `trackChanges` | boolean | true | Stores a snapshot per domain and reports changes on the next run. The first run creates the baseline. |
| `registrarLookup` | boolean | true | Query the registrar's RDAP for owner organisation/country (gTLDs). |
| `whoisFallback` | boolean | true | Use port-43 WHOIS when a TLD has no RDAP or its RDAP server is down. |
| `maxDomains` | integer | 1000 | Safety cap per run. |

Monitoring example:

```json
{
  "domains": ["google.com", "https://www.bbc.co.uk/news", "info@claude.ai", "nic.it", "denic.de"],
  "expiryWarningDays": 60,
  "outputFilter": "alerts_only",
  "trackChanges": true
}
```

Availability sweep example:

```json
{
  "names": ["lumora", "brightfield"],
  "tlds": ["com", "io", "ai", "co", "app"],
  "outputFilter": "available_only",
  "trackChanges": false
}
```

### Output

One dataset item per domain. Example (real data, shortened):

```json
{
  "domain": "google.com",
  "domainUnicode": "google.com",
  "input": "google.com",
  "tld": "com",
  "lookupStatus": "registered",
  "isRegistered": true,
  "source": "rdap",
  "registrar": "MarkMonitor Inc.",
  "registrarIanaId": "292",
  "registrarAbuseEmail": "abusecomplaints@markmonitor.com",
  "registrantOrganization": "Google LLC",
  "registrantCountry": "US",
  "registrantPrivacy": "public",
  "createdAt": "1997-09-15T04:00:00.000Z",
  "updatedAt": "2019-09-09T15:39:04.000Z",
  "expiresAt": "2028-09-14T04:00:00.000Z",
  "daysUntilExpiry": 715,
  "expiryStatus": "ok",
  "domainAgeDays": 10606,
  "statusCodes": ["clientDeleteProhibited", "clientTransferProhibited", "clientUpdateProhibited",
                  "serverDeleteProhibited", "serverTransferProhibited", "serverUpdateProhibited"],
  "transferLocked": true,
  "onHold": false,
  "pendingDelete": false,
  "dnssecSigned": false,
  "nameservers": ["ns1.google.com", "ns2.google.com", "ns3.google.com", "ns4.google.com"],
  "dnsProvider": "Google",
  "riskFlags": ["dnssec_unsigned"],
  "isAlert": false,
  "changes": [],
  "previousCheckAt": "2026-09-22T14:10:20.797Z",
  "notes": [],
  "sourceUrl": "https://rdap.verisign.com/com/v1/domain/google.com",
  "error": null,
  "checkedAt": "2026-09-29T14:10:37.236Z"
}
```

`lookupStatus` is one of `registered`, `available`, `unsupported_tld`, `invalid_input` or `error`.
`registrantPrivacy` is `public`, `redacted`, `proxy` or `null` (unknown). When something changed since the
previous run, `changes` lists it field by field, for example
`{"field": "registrar", "previous": "Old Registrar LLC", "current": "MarkMonitor Inc."}`.

The key-value store record `SUMMARY` holds counts per status, the expiring domains sorted by days left,
expired and available domains, all alerts with their changes, and failed lookups with the reason. The
dataset has three views: **Expiry overview**, **Registration details** and **Changes since previous run**.

#### What each registry publishes (tested live on google.<tld>, September 2026)

Coverage depends on what the registry itself makes public. The Actor never guesses: missing values are
`null`, with a note explaining why.

| TLDs | Source | Expiry date | Owner organisation |
|---|---|---|---|
| .com .net .org .info .xyz .ai .tv .cc .app .dev and other gTLDs | RDAP + registrar RDAP | yes | yes, unless redacted or proxied |
| .ca .fr .pl .in .sg .fi .si .cz .com.ar | RDAP | yes | varies (.ca, .fr, .pl often yes) |
| .uk | RDAP | yes | redacted |
| .nl .no .com.au | RDAP | **no** | .nl redacted |
| .io .co .me .us .it .se .dk .sk .ee .lt .hr .jp .tr .pt .cl .com.mx .com.tw .com.co | WHOIS | yes | varies (.io, .me, .us, .it, .jp, .cl often yes) |
| .de .eu .be .at .ro .bg .lv .hu .ae .co.nz .co.il | WHOIS | **no** | .lv, .ae yes for companies |
| .ch .li .es .gr .co.za | none | – | Returned as `unsupported_tld` (free). |

A few registries (for example .ru, .cn, .com.hk, .co.kr, .com.br, .co.id) only answer some networks. If
they are unreachable from the Apify platform, the domain comes back as `error` with the reason, free of
charge.

### Use cases

- **Domain portfolio management:** never lose a domain to a missed renewal. Schedule weekly with
  `alerts_only` and get only what is expiring, in auto-renew grace or changed.
- **Brand protection:** watch look-alike domains and get alerted when one is newly registered, changes
  owner or nameservers, or drops. Sweep a new brand name across TLDs before launch.
- **Security and fraud triage:** flag newly registered domains (`newly_registered`, `domainAgeDays`), find
  the registrar abuse contact, and detect a removed transfer lock or a registrar change on your own domains.
- **Sales and market intelligence:** registrant organisation, registrar and DNS provider (Cloudflare,
  Route 53, Azure, Shopify, Wix, ...) for a list of company domains.
- **M\&A, KYB and vendor due diligence:** ownership, registration age, registrar and lock status.
- **Domain investing:** check availability, parked domains and pending-delete status in bulk.

### Integrations and scheduling

Create a task with `outputFilter: alerts_only`, schedule it (for example weekly), and attach an Apify
integration (e-mail, Slack, webhook, Zapier or Make) that fires when the run succeeds. You then only get a
message with items when a domain needs attention. The `SUMMARY` record is a compact JSON for dashboards.

### Pricing

Pay per event: **$0.002 per domain result** ($2 per 1,000 domains), with no start fee. The registrar owner
lookup and the WHOIS fallback are included. You only pay for successful lookups (`registered` or
`available`). Errors, unsupported TLDs and invalid input are free. With `alerts_only` or `available_only`
you only pay for the results you receive. A weekly check of 100 domains costs at most $0.87 per month
($0.20 per run), and less in alerts-only mode.

### Legal

The Actor only uses the official, public RDAP and WHOIS services of registries and registrars, which are
designed for automated queries (RFC 9082/9083). It keeps request rates modest per server and does not work
around blocks. Personal data is kept out on purpose: of the registrant, only the **organisation name**
and **country** are returned. Names, e-mail addresses, phone numbers and street addresses are never output,
even when a registry publishes them. Registrar abuse addresses are role contacts of the registrar company.
You remain responsible for how you use the results, including each registry's terms of use.

### FAQ

**Why is `expiresAt` empty for my .nl / .de / .eu domain?**
Those registries do not publish expiry dates publicly. Domains in these TLDs renew automatically through
the registrar, so ask your registrar for the renewal date. The record still shows registration status,
registrar (where published), status and nameservers.

**What does `auto_renew_grace` mean?**
The domain passed its expiry date and the registry renewed it automatically, but the registrar can still
delete it during the grace period (usually up to 45 days) if the renewal is not paid. The new `expiresAt`
can then be misleading, which is why this is an alert.

**Why is the owner shown for google.com but not for my domain?**
Most registrars redact individual registrants under GDPR, and many domains use a privacy/proxy service
(`registrantPrivacy: proxy`). Company registrations at corporate registrars are often public.

**What is the difference between WHOIS and RDAP?**
RDAP is the modern, standardised JSON replacement for WHOIS. ICANN made it mandatory for gTLDs and
retired port-43 WHOIS for them in 2025. This Actor prefers RDAP and uses WHOIS only as a fallback.

**Does `available` mean I can register the domain?**
It means the registry has no registration for it. Some names are still reserved or blocked by the
registry, so a registrar may refuse them. Premium pricing is not checked.

**How does change tracking work?**
With `trackChanges` on, a snapshot per domain is stored in a named key-value store. The next run compares
registration status, registrar, owner organisation, expiry date, significant status codes, nameservers and
DNSSEC. The first run is the baseline (`changes: null`).

**Can I check thousands of domains?**
Yes. Requests are spread across registries with a small delay per server. Raise `maxDomains` if you need
more than 1,000 per run.

**Why do I see `dnssec_unsigned` on big-brand domains?**
Most domains are not DNSSEC-signed. It is an informational flag, not an alert.

### Related Actors

- [Website Screenshot Monitor](https://apify.com/codeclouds/website-screenshot-monitor): visual change monitoring for the websites behind your domains.
- [Brand Mention News Monitor](https://apify.com/codeclouds/brand-mention-news-monitor): pair it with domain monitoring for brand protection.
- [Container CVE Matcher](https://apify.com/codeclouds/container-cve-matcher): more security tooling for your infrastructure.

### Keywords

whois lookup, bulk whois, rdap lookup, domain owner lookup, domain expiry checker, domain expiration
monitor, domain expiry alert, domain availability checker, brand name availability, registrar lookup,
domain age checker, nameserver lookup, dns provider lookup, dnssec check, domain portfolio monitoring,
brand protection, newly registered domains, epp status codes

### Changelog

#### 0.1.0

- First release: RDAP via the IANA bootstrap plus the registrar's RDAP for owner organisation/country, and
  a WHOIS fallback tested on 30+ registry formats (also used when an RDAP server is down).
- Expiry status, risk flags, DNS provider detection, and change tracking between runs with noise
  suppression.
- Output modes `all`, `alerts_only` and `available_only`, brand-name availability sweep, and a run summary.

# Actor input Schema

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

One domain per line (comma- or space-separated lists also work). Accepts bare domains (example.com), subdomains and URLs (https://www.bbc.co.uk/news is checked as bbc.co.uk), e-mail addresses (info@acme.io) and internationalized domains (bücher.de). Duplicates are checked once. When you use the brand-name sweep below, clear this list (the example domains are ignored automatically then).

## `names` (type: `array`):

Domain availability sweep: each name is combined with every TLD in "TLDs to check" (acme → acme.com, acme.io, acme.ai, ...). Combine with Output = Available domains only to get just the free ones.

## `tlds` (type: `array`):

TLDs used for the brand-name sweep, without the dot. Only used when "Brand names" is filled.

## `expiryWarningDays` (type: `integer`):

A registered domain that expires within this many days gets expiryStatus = expiring\_soon and counts as an alert.

## `outputFilter` (type: `string`):

all: one result per domain. alerts\_only: only domains that are expired, expiring soon, in auto-renew grace, pending delete or transfer, on hold, or whose registrar, owner, nameservers, status or registration changed since the previous run (cheapest for scheduled monitoring; you pay only for delivered results). available\_only: only unregistered domains (for availability sweeps). Errors, unsupported TLDs and invalid entries are always included, free. The full overview is always in the SUMMARY record.

## `trackChanges` (type: `boolean`):

Stores a snapshot per domain so the next run can report changes (renewed, registrar\_changed, owner\_changed, nameservers\_changed, status\_changed, transfer\_lock\_removed, dropped, newly\_taken). The first run creates the baseline (changes = null). Most useful on a schedule; a single ad-hoc run only creates the baseline.

## `registrarLookup` (type: `boolean`):

For registries that keep owner data at the registrar (.com, .net and others), also query the registrar's RDAP server to get the registrant organisation and country (e.g. google.com → Google LLC, US) and detect privacy/proxy services. Adds one request per domain; failures never fail the domain.

## `whoisFallback` (type: `boolean`):

Many ccTLDs (.de, .eu, .io, .be, .co, .it, .se, .dk, .jp and more) have no RDAP service. When enabled, the registry's official port-43 WHOIS is queried instead, also when an RDAP server is unreachable. Data depth then depends on the registry (e.g. .de and .eu publish no expiry date). .ch, .li and .es refuse automated WHOIS and are reported as unsupported\_tld.

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

Safety cap on the number of domains checked in one run (after combining domains and brand-name sweep).

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "wikipedia.org",
    "bbc.co.uk",
    "claude.ai",
    "sidn.nl"
  ],
  "names": [],
  "tlds": [
    "com",
    "net",
    "org",
    "io",
    "ai",
    "co",
    "app",
    "dev"
  ],
  "expiryWarningDays": 30,
  "outputFilter": "all",
  "trackChanges": true,
  "registrarLookup": true,
  "whoisFallback": true,
  "maxDomains": 1000
}
```

# Actor output Schema

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

All dataset items: one record per checked domain.

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

Counts per lookup status plus the lists of expiring, expired and alerting domains for 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": [
        "google.com",
        "wikipedia.org",
        "bbc.co.uk",
        "claude.ai",
        "sidn.nl"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("codeclouds/whois-rdap-domain-expiry-monitor").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": [
        "google.com",
        "wikipedia.org",
        "bbc.co.uk",
        "claude.ai",
        "sidn.nl",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("codeclouds/whois-rdap-domain-expiry-monitor").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": [
    "google.com",
    "wikipedia.org",
    "bbc.co.uk",
    "claude.ai",
    "sidn.nl"
  ]
}' |
apify call codeclouds/whois-rdap-domain-expiry-monitor --silent --output-dataset

```

## MCP server setup

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

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/Y8cUzRbwvthGHmqnJ/builds/walL2P8Ag7WARAYjU/openapi.json
