# RDAP Domain Lookup & Expiry/Change Monitor (`keepable_injera/rdap-domain-monitor`) Actor

Bulk WHOIS/RDAP lookup via the public RDAP protocol: registrar, status, created/updated/expiry dates, nameservers and DNSSEC. Expiry alerts and change detection with onlyNew mode. Unofficial; not affiliated with IANA or any registry.

- **URL**: https://apify.com/keepable_injera/rdap-domain-monitor.md
- **Developed by:** [Chee Chiaw Lim](https://apify.com/keepable_injera) (community)
- **Categories:** Developer tools, SEO tools, Automation
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$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

## RDAP Domain Lookup & Expiry/Change Monitor

Look up domains through public registry RDAP servers. Read registration and expiry dates, registrar details, status codes, nameservers, and DNSSEC status. Schedule repeat runs to detect changes.

The Actor uses IANA's DNS bootstrap to find each server. It caches the bootstrap for 24 hours in the named state store. It requires no login and collects no registrant data.

### Input

```json
{
  "domains": ["example.com", "google.com", "apify.com", "wikipedia.org", "ngrok.io"],
  "onlyNew": false,
  "expiresWithinDays": 30,
  "includeRaw": false,
  "maxItems": 100,
  "minIntervalMs": 1000,
  "stateStoreName": "rdap-domain-monitor-state"
}
```

| Field | Default | Meaning |
| --- | --- | --- |
| `domains` | Required | Domain list. The runtime also accepts a newline or comma separated string. Schemes, paths, and trailing dots are removed. Names become lowercase ASCII with IDN conversion. Duplicates are removed. |
| `onlyNew` | `true` | Emit only new or changed domains. Set false for a full lookup report. |
| `expiresWithinDays` | `30` | Alert window in days, from 0 to 36500. Already expired domains also alert. |
| `includeRaw` | `false` | Add the public RDAP response as `raw`. |
| `maxItems` | `100` | Maximum emitted records, including errors, from 1 to 10000. Unchanged domains do not count toward this limit. |
| `minIntervalMs` | `1000` | Minimum time between request starts per host, from 0 to 60000 milliseconds. |
| `stateStoreName` | `rdap-domain-monitor-state` | Named KV store for bootstrap cache and domain snapshots. Use a separate name for each independent history. |

### Output sample

This illustrative record shows a domain's first successful lookup.

```json
{
  "domain": "example.com",
  "tld": "com",
  "rdapServer": "https://rdap.verisign.com/com/v1",
  "found": true,
  "registrar": "Example Registrar",
  "registrarIanaId": "123",
  "statuses": ["clientTransferProhibited"],
  "createdAt": "1995-08-14T04:00:00.000Z",
  "updatedAt": "2026-10-01T00:00:00.000Z",
  "expiresAt": "2026-11-01T00:00:00.000Z",
  "daysToExpiry": 12,
  "expiresWithinAlert": true,
  "nameservers": ["a.iana-servers.net", "b.iana-servers.net"],
  "dnssec": true,
  "fingerprint": "01bf303a56dc8040ddb2b96ebc74521940f6b2b0975b0e7bf85eeec3be1467d6",
  "changeType": "new",
  "changedFields": [],
  "previous": null,
  "error": null,
  "checkedAt": "2026-10-20T00:00:00.000Z",
  "rdapUrl": "https://rdap.verisign.com/com/v1/domain/example.com"
}
```

Dates are ISO timestamps or null when unavailable. `daysToExpiry` rounds up the remaining time in days and can be negative. `dnssec` is null when the server does not report delegation signing. HTTP 404 produces `found: false` with `error: null`. A missing server produces `error: "no RDAP server for TLD"`. Other lookup failures produce an error string and still appear in the dataset.

The dataset has overview, changes, and details views. `RUN_SUMMARY` in the default KV store contains start and finish times, `onlyNew`, counts for fetched, eligible, and pushed records, and `stoppedAtChargeLimit`. Failed runs also include `error`.

### Change monitoring

The SHA-256 fingerprint covers `registrar`, sorted `statuses`, `expiresAt`, sorted lowercase `nameservers`, `dnssec`, and `found`. Changes to `updatedAt`, registrar IANA ID, registration dates, or the daily expiry countdown do not trigger an emission.

`changeType` is `new`, `changed`, or `unchanged`. `changedFields` lists changed fingerprint fields. `previous` contains their old values, or null when none changed. New records have an empty `changedFields` list. With `onlyNew: true`, an unchanged domain stays suppressed even when its expiry alert becomes true. Lookup errors always emit and leave successful history intact.

The named store holds `BOOTSTRAP` with `{ fetchedAt, data }` and `DOMAINS` with a per-domain `{ fingerprint, fields }` map. Snapshots save only after a successful dataset push. Full lookup runs also update history. Delete `DOMAINS` to reset history.

Dataset pushes and state saves are separate operations. A crash between them can produce a duplicate on retry. Avoid overlapping runs that share a state store.

### Pricing

The pay-per-event price is $0.002 per domain, or $2 per 1,000 domains, using the `domain` event. There is no run fee. Errored lookups are pushed without an event and are not charged. Successful 404 lookups count as results and are charged. Suppressed unchanged records are not charged. Local runs do not bill you. Store pricing must match the event configuration in `PUBLISHING.md` before publication.

### Limits

Some ccTLDs do not offer RDAP through IANA's bootstrap. Registries may rate limit requests or omit fields. Contacts may be redacted. The normalized output includes no registrant data. Opting into `raw` includes whatever public response the registry provides.

The Actor retries HTTP 429 and 5xx responses up to three times with exponential backoff and honors `Retry-After`. Each request times out after 30 seconds. Registry data can lag behind registration changes. Expiry alerts depend on the dates the registry publishes and do not guarantee renewal or availability.

### Disclaimer

This Actor is unofficial and is not affiliated with or endorsed by IANA, ICANN, or any registry or registrar. It uses only the public RDAP protocol. It uses no logos and requires no login.

### Local development

```sh
npm install
npm test
npx apify run
```

Node 20 or newer is required. The CLI reads `storage/key_value_stores/default/INPUT.json`, which matches the root `INPUT.json` sample. `npm start` also runs the Actor with local storage. Tests use `node:test`, stubbed fetch, and temporary local storage without network access.

# Actor input Schema

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

Domain names. The runtime also accepts a newline or comma separated string.

## `onlyNew` (type: `boolean`):

Emit only new or changed records. Expiry alerts do not override this filter.

## `expiresWithinDays` (type: `integer`):

Alert when expiry is within this many days, including already expired domains.

## `includeRaw` (type: `boolean`):

Include the public RDAP response in raw.

## `stateStoreName` (type: `string`):

Named KV store for bootstrap cache and domain history.

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

Maximum dataset records per run, including lookup errors.

## `minIntervalMs` (type: `integer`):

Milliseconds between request starts on each host.

## Actor input object example

```json
{
  "onlyNew": true,
  "expiresWithinDays": 30,
  "includeRaw": false,
  "stateStoreName": "rdap-domain-monitor-state",
  "maxItems": 100,
  "minIntervalMs": 1000
}
```

# Actor output Schema

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

No description

## `runSummary` (type: `string`):

No description

## `changes` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("keepable_injera/rdap-domain-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("keepable_injera/rdap-domain-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 '{}' |
apify call keepable_injera/rdap-domain-monitor --silent --output-dataset

```

## MCP server setup

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