# Domain Expiration Monitor: RDAP, WHOIS Lookup & DNSSEC (`adastra-connect/rdap-domain-monitor`) Actor

A domain expiration monitor for domain expiry, registrar and DNSSEC data using RDAP, the official replacement for WHOIS domain lookup.

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

## Pricing

from $1.10 / 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?

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

## Monitor domain expiry and registration changes with RDAP Domain & Expiry Monitor

Check a domain portfolio for expiration dates, registrar changes, transfer locks, nameservers and DNSSEC registration state. RDAP Domain & Expiry Monitor queries official public registry RDAP services discovered through the IANA bootstrap. Domain portfolio managers, DevOps and SRE teams, brand protection teams and MSPs can export consistent rows or schedule change alerts without collecting personal contact data.

### Domain registration and expiry data you can get

| Fields | Meaning |
| --- | --- |
| `domain`, `unicodeDomain`, `tld` | Normalized ASCII domain, Unicode display name and TLD |
| `rdapServer`, `sourceUrl`, `attribution` | Registry bootstrap base, exact response URL and source credit |
| `lookupStatus`, `handle` | Registered or optional not-found result, published registry handle |
| `registrarName`, `registrarIanaId` | Public registrar business identity only |
| `registeredAt`, `expiresAt`, `lastChangedAt` | Published registration, registry expiry and change timestamps |
| `daysUntilExpiry` | Whole days remaining, rounded down; negative after expiry |
| `statusCodes`, `nameservers` | Sorted RDAP statuses and lowercase nameserver hostnames |
| `dnssecSigned`, `dnssecDsRecords` | Published delegation signature state and DS key tags, algorithms and digest types |
| `signals`, `changes`, `checkedAt` | Alert flags, monitored field changes and UTC observation time |

Missing values are `null` or empty arrays. Registrant, administrative, technical, billing and abuse contacts are excluded. Notices, remarks, phone numbers, emails, addresses and full DS digests are omitted.

### How to use the domain expiry monitor

1. Enter domain names or website URLs in `domains`.
2. Set an expiry threshold and maximum result count.
3. Run the Actor and review the dataset table or export JSON, CSV or spreadsheet data.
4. Open `RUN_SUMMARY` for blocked, unsupported, rate-limited or unavailable targets.
5. Enable `monitorMode` and schedule future runs with the same `monitorKey` for change alerts.

```json
{
  "domains": ["openai.com", "cloudflare.com", "iana.org"],
  "expiringWithinDays": 30,
  "onlyFlagged": false,
  "includeNotFound": false,
  "maxResults": 25,
  "monitorMode": false,
  "monitorKey": "default",
  "requestDelayMs": 750
}
```

Empty API input uses these three domains and a default `maxResults` of 100. The editor prefill uses 25. Domain URLs and subdomains normalize to the registrable domain using the public suffix list, so a hostname under `example.co.uk` becomes `example.co.uk`. Unknown suffixes retain the exact hostname. Duplicate normalized domains are queried once. Credentials, IP literals and malformed domains are rejected or recorded as invalid individual targets.

### Sample domain registration output

Real local snapshot on 2026-10-25, shortened to useful columns:

| Domain | Registrar | Registry expiry UTC | DNSSEC signed |
| --- | --- | --- | --- |
| `openai.com` | MarkMonitor Inc. | 2029-01-19 19:28:24 | false |
| `cloudflare.com` | Cloudflare, Inc. | 2033-02-17 22:07:54 | true |
| `iana.org` | CSC Corporate Domains, Inc. | 2027-12-08 17:00:53 | true |

A shortened row:

```json
{
  "domain": "iana.org",
  "unicodeDomain": "iana.org",
  "tld": "org",
  "lookupStatus": "registered",
  "registrarName": "CSC Corporate Domains, Inc.",
  "registrarIanaId": "299",
  "expiresAt": "2027-12-08T17:00:53.000Z",
  "daysUntilExpiry": 408,
  "statusCodes": [
    "server delete prohibited",
    "server transfer prohibited",
    "server update prohibited"
  ],
  "dnssecSigned": true,
  "dnssecDsRecords": [{ "keyTag": 2234, "algorithm": 13, "digestType": 2 }],
  "signals": [],
  "changes": []
}
```

### Expiration, transfer lock and DNSSEC alerts

`expiring_soon` means `0 <= daysUntilExpiry <= expiringWithinDays`. `expired` means the published registry expiry timestamp is in the past. `transfer_unlocked` means neither client nor server transfer-prohibited status was published. It does not establish transfer eligibility. `dnssec_unsigned` is emitted only for an explicit false signature state; unknown DNSSEC state stays `null`.

Other flags are `client_hold`, `server_hold`, `pending_delete` and `redemption_period`. Both spaced RDAP status strings and equivalent EPP camelCase forms are recognized. `onlyFlagged` requires at least one flag, so unsigned domains may qualify even when their expiry date is far away.

### Incremental runs and portfolio monitoring

The first successful observation of a domain emits `changes: ["initial"]`. Later runs emit only when expiry, registrar name or ID, status codes, nameservers, DNSSEC state or DS metadata, lookup status, or signals change. A domain newly entering the expiry threshold is emitted because its signals change. A daily countdown alone does not create another row. Snapshot mode always uses `changes: []`.

Baselines persist in the named `rdap-domain-monitor-state` store. Use the same `monitorKey` and threshold for the same watch. Changing either starts a separate baseline. Failed lookups and filtered-out rows do not overwrite the last delivered baseline. Not-found observations participate only with `includeNotFound: true`.

Run one schedule at a time per watch. Delivery reservations reduce duplicate billing from overlapping runs, but there is no transaction across the dataset, charging, queue and state. A process crash between reservation and delivery can leave a transition reserved. Use a new monitor key to recover that watch. Large portfolios may need separate runs because each run collects for at most 45 seconds. Domains are processed in input order, so rotate or partition portfolios when the time budget prevents full coverage.

### Pricing

Price: about US$1.50 per 1,000 emitted domain results, plus the Actor start event. The Store page shows the final price.

One emitted dataset row is one `domain` event. Only registered domains are emitted by default. With `includeNotFound: true`, emitted 404 observations are also billed. Unchanged observations in monitor mode, filtered rows, duplicates, unsupported TLDs and upstream errors are not billed. The Actor respects `maxResults` and stops gracefully when the event spending allowance is reached. This Actor is intended for pay-per-event pricing without additional platform usage charges.

### Scheduling and integrations

Use Apify schedules for recurring checks, webhooks for run completion and the Apify API for exports. Connect datasets to Make, Zapier or n8n, or let AI agents access them through MCP. A completed run can have zero rows, so integrations should inspect `RUN_SUMMARY` as well as the dataset.

### Is it legal?

The Actor uses official public HTTPS endpoints without login, cookies, browser automation or proxies. It checks robots rules, limits requests and respects retry instructions. Registry terms still apply and may restrict automated or high-volume access and redistribution. Public access is not permission for unrestricted reuse. Use it for a legitimate portfolio-management purpose and review the terms applicable to your domains. This is not legal advice; users are responsible for their use of the data.

### Limitations

- Some TLDs have no HTTPS IANA bootstrap entry. A built-in common-TLD fallback is used only when IANA is unreachable; fallback coverage is best effort.
- Robots denial, 429, 5xx, redirects or HTML on HTTP 200 prevents lookup. Other 4xx responses mean no policy under RFC 9309. Verisign returns HTTP 400 for robots; all three default domains succeeded in local verification.
- Registry expiration can differ from a registrar's customer renewal deadline. Auto-renew and redemption periods affect interpretation. Verify critical renewals with your registrar.
- DNSSEC metadata does not prove that the live DNS chain validates. Nameservers are registry records, not live DNS checks.
- A 404 is not proof that a domain is available to buy. Reserved, premium and restricted names can return incomplete information.
- The bootstrap is cached for 24 hours. Requests have a 10-second timeout and at most one retry with a wait capped at 10 seconds, subject to the run's overall time budget. There is no RDAP search or pagination.

### FAQ

**Does this replace WHOIS contact lookup?** No. It deliberately omits personal contact records and uses RDAP only.

**Why is a domain missing from my dataset?** Check `RUN_SUMMARY`. It may be unchanged, filtered, unsupported, blocked, rate-limited or unavailable, or the run may have reached its result or time limit.

**Does an all-upstream-failure run fail?** No. It finishes successfully with zero rows and summary status `upstream_unavailable`. Invalid overall input and dataset or persistent-state errors fail the run.

**Can I monitor internationalized domains?** Yes. ASCII punycode is queried and a Unicode display form is returned.

**Can I reset monitoring?** Use a new `monitorKey`. Deleting the persistent stores also removes saved baselines and reservations.

### Support

Use the Issues tab on this Actor's Apify Store page. Include the run ID and a short description of the affected domain or input setting.

Apify, OpenAI, Cloudflare, IANA, ICANN, Verisign, Public Interest Registry, CSC Corporate Domains, MarkMonitor, Make, Zapier and n8n are trademarks or names of their respective owners. This Actor is not affiliated with, endorsed or sponsored by those organizations. Other registry and registrar names remain the property of their owners.

# Actor input Schema

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

Domain names or URLs. Subdomains become registrable domains using the public suffix list. Unknown suffixes retain the exact hostname.

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

Flag domains expiring in this many days, including today.

## `onlyFlagged` (type: `boolean`):

Emit only domains with at least one signal.

## `includeNotFound` (type: `boolean`):

Emit and bill 404 results as not_found. A 404 is not a guarantee that a domain can be purchased.

## `maxResults` (type: `integer`):

Hard cap on rows per run.

## `monitorMode` (type: `boolean`):

Emit first observations and changed domains only, using a persistent named state store.

## `monitorKey` (type: `string`):

Namespace for a portfolio watch. Use a new key to reset its baseline.

## `requestDelayMs` (type: `integer`):

Minimum delay per registry host. Robots crawl delays can increase it.

## Actor input object example

```json
{
  "domains": [
    "openai.com",
    "cloudflare.com",
    "iana.org"
  ],
  "expiringWithinDays": 30,
  "onlyFlagged": false,
  "includeNotFound": false,
  "maxResults": 25,
  "monitorMode": false,
  "monitorKey": "default",
  "requestDelayMs": 750
}
```

# Actor output Schema

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

No description

## `overview` (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": [
        "openai.com",
        "cloudflare.com",
        "iana.org"
    ],
    "maxResults": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("adastra-connect/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 = {
    "domains": [
        "openai.com",
        "cloudflare.com",
        "iana.org",
    ],
    "maxResults": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("adastra-connect/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 '{
  "domains": [
    "openai.com",
    "cloudflare.com",
    "iana.org"
  ],
  "maxResults": 25
}' |
apify call adastra-connect/rdap-domain-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adastra-connect/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/gNjGq7JwO2UNbOWmc/builds/rDinj9V87OZIkvnhl/openapi.json
