# WHOIS / RDAP Domain Scraper & Change Monitor (`automa-flow/domain-rdap-change-monitor`) Actor

Run bulk WHOIS/RDAP domain lookups and monitor expiry, renewals, registrar transfers, nameservers, DNSSEC, DNS and SSL/TLS certificates. Get authoritative registry data, exact change events, safe NOT\_FOUND confirmation and no registrant PII.

- **URL**: https://apify.com/automa-flow/domain-rdap-change-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (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 $0.80 / 1,000 verified domain checks

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

## WHOIS / RDAP Domain Scraper & Change Monitor

Run authoritative bulk WHOIS/RDAP domain lookups and monitor expiry, renewals,
registrar transfers, nameservers, DNSSEC, DNS and TLS changes across scheduled
runs.

This Actor turns a domain watchlist into authoritative, normalized observations
and exact change events. It discovers each registry through the official IANA
RDAP bootstrap, stores only the last complete successful state in Apify KVS, and
never exposes registrant personal data.

Unlike a raw WHOIS scraper, it produces history-ready rows with explicit
`SUCCESS`, `PARTIAL`, `NOT_FOUND`, `RATE_LIMITED` and failure semantics. A bad
check never overwrites the last known good state, and a single 404 never becomes
a false domain-drop alert.

### What it is for

- audit a single domain or a portfolio of up to 1,000 inputs;
- detect registration renewals and expiry-date changes;
- alert once when 90/30/14/7/1-day expiry thresholds are crossed;
- detect registrar, registry nameserver, EPP status and DNSSEC changes;
- optionally observe public A, AAAA, MX, NS and CAA records;
- optionally monitor standard port-443 SSL/TLS certificate expiry and renewal;
- feed scheduled runs into webhooks, Slack/email automation, a SIEM or a CMDB.

This is not a WHOIS text parser, domain-availability oracle, vulnerability
scanner or ownership/registrant data extractor.

### Quick start: WHOIS/RDAP lookup

```json
{
  "domains": [{"domain": "example.com"}],
  "mode": "snapshot",
  "outputMode": "all",
  "includeDns": false,
  "includeTls": false
}
```

The Actor accepts bare domains, subdomains and HTTP(S) URLs. A pinned Public
Suffix List converts all of these to the same registration:

```text
example.com
www.example.com
https://www.example.com/path
  -> example.com
```

IDNs are normalized to lowercase A-label/punycode form. `foo.example.co.uk`
becomes `example.co.uk`; suffixes are never parsed with a hand-written last-two-
labels rule.

### Monitor a 100-domain portfolio

Use the same input on every scheduled run and keep `mode: monitor`:

```json
{
  "domains": [
    {"domain": "example.com", "externalId": "portfolio-001"},
    {"domain": "example.org", "externalId": "client-17"}
  ],
  "mode": "monitor",
  "outputMode": "changesOnly",
  "includeDns": true,
  "includeTls": true,
  "expiryWarningDays": [90, 30, 14, 7, 1]
}
```

Extend `domains` to 100 or 1,000 objects. Duplicate normalized domains are
looked up once but still receive one result row per input/external ID. One bad
domain never aborts the batch.

`outputMode: changesOnly` suppresses the redundant `UNCHANGED` object inside
`events`, while `all` keeps it. Both deliberately emit one status row per input
so an empty dataset can never disguise a failed source or an unchanged
portfolio. Trigger alerts only for non-empty `events`; baseline, failure and
change classifications remain explicit in the main row.

### Expiry monitoring without alert spam

Warnings fire only when the observed remaining days cross a configured
threshold:

```text
31 days -> 29 days
event: EXPIRY_WARNING
warning_code: EXPIRY_WARNING_30D
```

The same warning is not repeated on the next daily run. When RDAP reports a
later expiration date, the Actor emits both `DOMAIN_RENEWED` (with exact
`before`, `after` and `extension_days`) and `EXPIRY_DATE_CHANGED`, then resets
the applicable warning state.

### Registrar transfer example

```json
{
  "change_type": "CHANGED",
  "events": [
    {
      "type": "REGISTRAR_CHANGED",
      "before": {"name": "Registrar A", "iana_id": "1"},
      "after": {"name": "Registrar B", "iana_id": "2"}
    }
  ]
}
```

Registrar comparison uses the normalized registrar-role RDAP entity and its
IANA Registrar ID when available. Registrant/admin/technical vCards are dropped
before any state or Dataset record is built.

Changes to registry-published `created_at` and domain-level `updated_at` are
reported exactly as `REGISTRATION_DATE_CHANGED` and `RDAP_LAST_CHANGED`; the
RDAP database refresh timestamp is intentionally not used as domain state.

### Nameserver change example

Registry delegation and live DNS are separate signals:

```json
{
  "events": [
    {
      "type": "NAMESERVERS_CHANGED",
      "before": ["ns1.old.example", "ns2.old.example"],
      "after": ["ns1.new.example", "ns2.new.example"]
    },
    {
      "type": "NS_CHANGED",
      "before": ["ns1.old.example", "ns2.old.example"],
      "after": ["ns1.new.example", "ns2.new.example"]
    }
  ]
}
```

`NAMESERVERS_CHANGED` comes from RDAP. `NS_CHANGED` comes from the optional DNS
observation.

### Renewal detected

```json
{
  "type": "DOMAIN_RENEWED",
  "before": "2027-04-01T00:00:00Z",
  "after": "2028-04-01T00:00:00Z",
  "extension_days": 366
}
```

A change in RDAP `updated_at` alone is never treated as a renewal.

### Safe NOT\_FOUND handling

The statuses are:

- `SUCCESS` — RDAP and every requested DNS/TLS surface were verified;
- `PARTIAL` — RDAP succeeded but a requested DNS/TLS surface did not;
- `NOT_FOUND` — the authoritative RDAP service returned HTTP 404;
- `UNSUPPORTED_TLD` — current IANA bootstrap has no HTTPS RDAP route;
- `RATE_LIMITED` — the authoritative registry still returned 429 after bounded retries;
- `FAILED` — transport, HTTP, schema, input or unexpected isolated failure.

`NOT_FOUND` never means source failure. For a previously known domain, the first
authoritative 404 emits `NOT_FOUND_PENDING`. Only a second independent successful
authoritative confirmation emits `DOMAIN_EXPIRED_OR_DROPPED`. A timeout, 429,
500, malformed JSON or bootstrap failure never counts as confirmation and never
overwrites the last good state.

### DNS and SSL/TLS certificate scope

With `includeDns`, the Actor queries only public A, AAAA, MX, NS and CAA. It does
not dump TXT records. With `includeTls`, it makes one normal TLS connection to
port 443 and returns:

- SHA-256 certificate fingerprint;
- subject hostname and issuer;
- `valid_from`, `valid_to`, `days_to_expiry`;
- SAN count and deterministic hostname match.

There is no port scan, cipher scan, CVE lookup, SSL Labs clone or risk score.
Before TLS, resolved IPs are checked and private/reserved destinations are
rejected to prevent SSRF.

TLS is disabled by default because a valid registered domain does not
necessarily serve HTTPS on its bare hostname. Enable it only when that is part
of the state you want to monitor; a requested TLS failure is deliberately
`PARTIAL`, not silently ignored.

Rejected malformed inputs use an opaque placeholder in output. The original
invalid string is never echoed because it may itself contain credentials or
tokens.

### Run daily or weekly

First run the Actor once with the final monitor input. Then create an Apify
Schedule in Console, or call the official schedule endpoint. This daily example
uses placeholders and an Authorization header so no token appears in a URL:

```bash
curl -X POST "https://api.apify.com/v2/schedules" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "daily-domain-rdap-monitor",
    "isEnabled": true,
    "isExclusive": true,
    "cronExpression": "0 8 * * *",
    "timezone": "UTC",
    "actions": [{
      "type": "RUN_ACTOR",
      "actorId": "YOUR_ACTOR_ID",
      "runInput": {
        "body": "{\"domains\":[{\"domain\":\"example.com\",\"externalId\":\"brand-main\"}],\"mode\":\"monitor\",\"outputMode\":\"changesOnly\",\"includeDns\":true,\"includeTls\":true}",
        "contentType": "application/json; charset=utf-8"
      }
    }]
  }'
```

For a weekly portfolio review, change the expression to `0 8 * * 1`.

### Webhook event workflow

Attach an Actor-run-succeeded webhook in Apify Console to your HTTPS endpoint.
The webhook tells the receiver which run finished; read that run's default
Dataset, then keep rows where `change_type == "CHANGED"`. Every such row is
already an alert payload with `source_id`, `external_id`, `fingerprint`,
`scraped_at` and exact `events`.

Example receiver decision:

```javascript
for (const row of datasetItems) {
  if (row.change_type !== 'CHANGED') continue;
  await sendAlert({
    domain: row.domain,
    externalId: row.external_id,
    events: row.events,
    observedAt: row.scraped_at,
  });
}
```

### Use with AI agents through Apify MCP

Expose the Actor as a typed tool in an authenticated MCP-compatible client:

```text
https://mcp.apify.com?tools=automa-flow/domain-rdap-change-monitor
```

Example prompt:

```text
Run automa-flow/domain-rdap-change-monitor for these domains. Summarize expiry,
registrar, nameserver, DNSSEC, DNS, and TLS certificate facts; separate
NOT_FOUND from FAILED checks and cite each source_url.
```

After publication the same tool URL becomes discoverable to Store users. The
Actor intentionally excludes registrant personal data from its output.

### Output contract

One row describes one source observation at `scraped_at`. Stable identity and
history-ready fields are always present:

```text
source, source_id, source_url, scraped_at, schema_version, fingerprint
```

The fingerprint covers semantic registration and selected DNS/TLS state. It
excludes `scraped_at`, Actor/run IDs, headers, rate-limit metadata and derived
`days_to_expiry`, so an unchanged domain has the same fingerprint tomorrow.
Rows are append-only observations; the Actor does not build a historical
database. KVS stores only the last full success plus warning/drop-confirmation
metadata.

See [`examples/sample_output.json`](examples/sample_output.json) and the Dataset
views for observations, changes, expiry and failures.

### Coverage and limits

- RDAP coverage varies by registry/TLD and changes with the IANA bootstrap.
- Live research on 2026-09-01 found supported authoritative RDAP for `.com`,
  `.net`, `.org`, `.ai`, `.uk`, `.fr`, `.pl`, `.cz`, `.dev` and `.app`; `.io`,
  `.co`, `.de`, `.sk` and `.rs` had no current IANA bootstrap route.
- Some registries omit expiration, registrar ID or DNSSEC. Missing fields stay
  null/`UNKNOWN`; they are never invented.
- Registry rate limits are independent. The Actor uses concurrency 2 per
  authority, bounded exponential backoff and `Retry-After`; it never rotates
  proxies to evade limits.
- A domain with no working DNS/TLS can be a valid registration. Disable the
  optional surface if you want RDAP-only success/state for that watchlist.
- A 404 is not a guarantee that a domain is available to register. Confirm with
  a registrar before financial or legal decisions.

### Privacy, compliance and affiliation

Registrant name, email, phone, postal address, organization and all
registrant/admin/technical contact fields are intentionally excluded even if a
registry publishes them. No raw RDAP response or vCard is stored.

This Actor accesses public registry protocol endpoints and does not bypass
authentication, CAPTCHA, paywalls or access controls. Users are responsible for
registry terms, reasonable request rates and lawful downstream use. This Actor
is not affiliated with, endorsed by or operated by ICANN, IANA or any registry.

### Cost model

Direct RDAP, DNS and TLS use no browser and no proxy, so proxy cost is normally
`$0`. One unique supported domain uses one authoritative RDAP request; DNS adds
at most five RR queries and TLS adds one port-443 connection. Retries,
`FAILED`, `PARTIAL`, `RATE_LIMITED` and `UNSUPPORTED_TLD` rows are not successful
billable checks. An authoritative `NOT_FOUND` is a completed domain check.

Pay per event starts at **$0.001 per verified distinct domain check** on the Free
plan ($1.00 per 1,000) and decreases by Apify subscription tier:

| Apify plan | Price per verified domain | Price per 1,000 |
| --- | ---: | ---: |
| Free | $0.00100 | $1.00 |
| Bronze | $0.00090 | $0.90 |
| Silver | $0.00085 | $0.85 |
| Gold | $0.00080 | $0.80 |
| Platinum | $0.00075 | $0.75 |
| Diamond | $0.00070 | $0.70 |

There is no run-start fee. Duplicate aliases, retries, invalid inputs,
`PARTIAL`, `FAILED`, `RATE_LIMITED` and `UNSUPPORTED_TLD` results are not
charged. Measured 1,000-domain RDAP platform cost at 512 MB is approximately
$0.07 before conservative headroom, including compute, observed transfer,
Dataset writes and monitor KVS operations at current Free/Bronze platform unit
costs.

The run's `maxTotalChargeUsd` is enforced before network work. If the remaining
budget cannot cover another unique domain, that domain is not contacted and
receives an uncharged `FAILED` row with `MAX_TOTAL_CHARGE_REACHED`; prior monitor
state is preserved.

The production release gate measured a 1,000-domain RDAP batch plus a
100-domain RDAP/DNS/TLS batch; no raw registration payload or contact data was
retained in those QA artifacts.

# Actor input Schema

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

One to 1,000 domains or URLs. Subdomains and URLs are reduced to their registrable domain with a pinned Public Suffix List. externalId is echoed for joins and is never sent to registries.

## `mode` (type: `string`):

snapshot returns the current observation without KVS state. monitor compares with the last complete successful observation and emits exact changes.

## `outputMode` (type: `string`):

changesOnly suppresses the redundant UNCHANGED event object; all keeps it. For batch safety, both modes still return one explicit status row per input.

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

Observe normalized public A, AAAA, MX, NS and CAA records. TXT records are intentionally excluded. A DNS transport failure makes the observation PARTIAL and preserves prior state.

## `includeTls` (type: `boolean`):

Make one standard port-443 TLS connection and collect certificate identity, issuer and validity only. No vulnerability scanning. Disabled by default because many valid registrable domains do not serve TLS on the bare domain; a requested TLS failure makes the observation PARTIAL and preserves prior state.

## `expiryWarningDays` (type: `array`):

Emit domain and TLS expiry warnings only when the remaining days cross one of these thresholds. Emitted warnings are remembered and reset after renewal.

## Actor input object example

```json
{
  "domains": [
    {
      "domain": "example.com",
      "externalId": "brand-main"
    },
    {
      "domain": "https://www.example.org/path",
      "externalId": "client-17"
    }
  ],
  "mode": "monitor",
  "outputMode": "changesOnly",
  "includeDns": true,
  "includeTls": false,
  "expiryWarningDays": [
    90,
    30,
    14,
    7,
    1
  ]
}
```

# Actor output Schema

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

No description

## `changes` (type: `string`):

Webhook-ready event projection; in changesOnly mode, UNCHANGED status rows have an empty events array.

## `expiry` (type: `string`):

No description

## `failures` (type: `string`):

No description

## `runSummary` (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": [
        {
            "domain": "example.com",
            "externalId": "brand-main"
        },
        {
            "domain": "https://www.example.org/path",
            "externalId": "client-17"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/domain-rdap-change-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": [
        {
            "domain": "example.com",
            "externalId": "brand-main",
        },
        {
            "domain": "https://www.example.org/path",
            "externalId": "client-17",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/domain-rdap-change-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": [
    {
      "domain": "example.com",
      "externalId": "brand-main"
    },
    {
      "domain": "https://www.example.org/path",
      "externalId": "client-17"
    }
  ]
}' |
apify call automa-flow/domain-rdap-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/domain-rdap-change-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/zQ2wSSJAhyMmAKTD7/builds/G5GhM6hXXJ7jeU6Xf/openapi.json
