# Domain Monitor: Expiry, DNS & Email Security Alerts (`rendangmedan/domain-monitor`) Actor

Track domain expiry, registrar, DNS, SPF, DMARC & DKIM for any list of domains. Daily delta: get only what changed. Official RDAP data, no personal info.

- **URL**: https://apify.com/rendangmedan/domain-monitor.md
- **Developed by:** [Rendang Medan](https://apify.com/rendangmedan) (community)
- **Stats:** 2 total users, 1 monthly users, 22.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 domain-checkeds

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

## Domain Monitor (RDAP + DNS)

**Watch a list of domain names and be told what changed.** Registrar, EPP statuses, creation and
expiration dates, name servers, DNSSEC, DNS records and a full e-mail authentication audit
(SPF, DMARC, DKIM, MTA-STS, TLS-RPT, BIMI), collected from the **official sources only**: the
registries' RDAP servers (found through the IANA bootstrap registry) and the DNS.

Run it once for an inventory, or **schedule it daily** with delta mode on: every run compares
each domain with the previous run and outputs the changes (registrar transfer, new name servers,
MX switched, SPF or DMARC edited, status `clientHold` added, renewal done...) plus an alert when a
domain is about to expire.

No WHOIS scraping, no third-party database, **no personal data**: contact entities (registrant,
administrative, technical, billing) are never read.

### Who is it for?

- **IT and security teams**: catch an unexpected registrar transfer, name server or MX change
  (domain hijacking, DNS takeover, forgotten migration) the morning after it happens.
- **Brand protection and legal**: monitor a domain portfolio, statuses (locks, holds, pending
  delete) and expiration dates; never lose a domain to a missed renewal.
- **Agencies and MSPs**: one scheduled run per client portfolio, a dataset of changes to forward
  to Slack, e-mail or your ticketing tool.
- **E-mail deliverability**: audit SPF (syntax, `all` qualifier, the 10 DNS lookup limit), DMARC
  policy and reporting, DKIM on common selectors, MTA-STS and BIMI for hundreds of domains at once,
  and be told when someone edits a record.
- **Sales and research**: check registration data and mail setup of a list of company domains,
  without collecting any personal data.

### What you get for each domain

| Field | Description |
| --- | --- |
| `domain`, `unicodeDomain` | Registrable domain in punycode and in Unicode (`xn--caf-dma.fr` / `café.fr`). |
| `status` | `ok`, `not_registered` (RDAP 404), `rdap_unsupported` (the TLD has no RDAP server at IANA: DNS data only) or `error`. |
| `registrar` | `{ "name", "ianaId" }` of the sponsoring registrar. Nothing else from the registrar entity. |
| `statuses`, `eppStatuses` | RDAP statuses (`client transfer prohibited`...) and their EPP codes (`clientTransferProhibited`...). |
| `createdAt`, `updatedAt`, `expiresAt` | Registration events, normalised to UTC ISO 8601. |
| `daysUntilExpiration`, `expiryAlert` | Days left, and `true` when below your threshold. |
| `nameServers` | Name servers at the registry (RDAP). |
| `dnssec` | `signed`, from the RDAP `secureDNS` object and the DS records in DNS. |
| `dns.records` | `A`, `AAAA`, `MX`, `NS`, `TXT`, `CNAME`, `CAA`, `SOA`, parsed into clean values. |
| `emailAuth` | SPF, DMARC, DKIM, MTA-STS (policy file included), TLS-RPT, BIMI, and a list of `issues` with a severity. |
| `changed`, `changeTypes`, `changes`, `changeSummary` | What moved since the previous run (delta mode). |
| `firstSeen`, `previousCheckedAt` | Baseline information for delta mode. |
| `rdap`, `error` | Which RDAP server answered, HTTP status, attempts, and the error when there is one. |

#### Change types

`registrar_changed`, `nameservers_changed`, `mx_changed`, `spf_changed`, `dmarc_changed`,
`status_changed`, `expiration_changed`, `dnssec_changed`, `domain_registered`,
`domain_not_registered`, `dns_records_changed` (for the extra record types you choose to track),
`expiring_soon` and `expired`.

`expiring_soon` is raised when a domain enters the alert window (30 days by default) and again at
14, 7, 3 and 1 days, not on every run. A field that could not be measured in a run (registry down,
DNS timeout) keeps its last known value and never produces a false "removed" change.

### Input example

```json
{
  "domains": [
    "example.com",
    "https://www.iana.org/about",
    "AFNIC.fr",
    "café.fr",
    "mail.denic.de"
  ],
  "deltaMode": true,
  "onlyChanged": false,
  "expiryAlertDays": 30,
  "stateStoreName": "my-portfolio",
  "extraTrackedRecordTypes": ["CAA"],
  "dkimSelectors": ["selector1", "selector2", "google"]
}
```

URLs, e-mail addresses, upper case, sub-domains and internationalised names are accepted. Each
value is reduced to its registrable domain with the embedded Public Suffix List
(`https://shop.example.co.uk/x` becomes `example.co.uk`) and duplicates are checked once.

Main options:

- `deltaMode` (default `true`): keep the state in a key-value store and report changes. Use one
  store per watch list. Pick it in `stateStore` (works with the Actor's limited permissions, see
  below) or give a name in `stateStoreName` (needs full permissions).
- `onlyChanged`: write only changed domains, first-seen domains and failures to the dataset.
- `expiryAlertDays`: alert threshold in days (`0` disables it).
- `includeRdap`, `includeDns`, `includeEmailAuth`, `dnsRecordTypes`: choose what to collect.
- `maxConcurrency`, `rdapMaxConcurrencyPerServer`, `rdapMinDelayMs`, `maxRetries`: politeness.
  The defaults (2 parallel requests and 300 ms spacing per registry) are gentle on purpose.
- `dnsMode`: `auto` (standard DNS, DNS over HTTPS as a fallback), `system` or `doh`.
- `rdapServerOverrides`: advanced, map a TLD to an RDAP server that IANA does not list yet.

### Output example

One dataset item per domain (shortened):

```json
{
  "domain": "example.com",
  "unicodeDomain": "example.com",
  "status": "ok",
  "billable": true,
  "checkedAt": "2026-10-12T06:00:04Z",
  "registered": true,
  "registrar": { "name": "RESERVED-Internet Assigned Numbers Authority", "ianaId": "376" },
  "statuses": ["client delete prohibited", "client transfer prohibited", "client update prohibited"],
  "eppStatuses": ["clientDeleteProhibited", "clientTransferProhibited", "clientUpdateProhibited"],
  "createdAt": "1995-08-14T04:00:00Z",
  "updatedAt": "2026-08-14T07:01:39Z",
  "expiresAt": "2027-08-13T04:00:00Z",
  "daysUntilExpiration": 305,
  "expiryAlert": false,
  "nameServers": ["a.iana-servers.net", "b.iana-servers.net"],
  "dnssec": { "signed": true, "rdapDelegationSigned": true, "dsRecordPresent": true },
  "dns": {
    "status": "ok",
    "resolver": "udp",
    "records": {
      "A": ["192.0.2.10"],
      "MX": [{ "priority": 0, "exchange": "." }],
      "NS": ["a.iana-servers.net", "b.iana-servers.net"],
      "TXT": ["v=spf1 -all"],
      "SOA": { "primaryNameServer": "ns.icann.org", "serial": 2026081401, "refresh": 7200, "retry": 3600, "expire": 1209600, "minimum": 3600 }
    }
  },
  "emailAuth": {
    "spf": { "status": "ok", "present": true, "record": "v=spf1 -all", "allQualifier": "fail", "dnsLookups": 0 },
    "dmarc": { "status": "ok", "present": true, "policy": "reject", "record": "v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s" },
    "dkim": { "status": "not_found", "selectorsFound": [] },
    "mtaSts": { "status": "missing", "present": false },
    "bimi": { "present": false },
    "issueCodes": []
  },
  "firstSeen": false,
  "previousCheckedAt": "2026-10-11T06:00:03Z",
  "changed": true,
  "changeTypes": ["nameservers_changed"],
  "changeSummary": "Name servers at the registry changed: added c.iana-servers.net; removed b.iana-servers.net",
  "error": null
}
```

Values above are illustrative. A failed domain gives one item with `status: "error"`,
`billable: false` and an `error` object (`code`, `message`), for example
`rdap_rate_limited` or `dns_failed`. A run summary (counters by status and by change type) is
saved in the default key-value store under `SUMMARY`.

The dataset has five views in Apify Console: **Overview**, **Changes**, **E-mail authentication**,
**DNS records** and **Statuses and errors**.

### Daily monitoring in three steps

1. Create a **Task** of this Actor with your domain list, `deltaMode` on and a dedicated
   `stateStoreName` (for example `client-acme`).
2. In **Schedules**, run the task every day, for example with the cron expression `0 6 * * *`.
   The first run builds the baseline; from the second run on, `changes` lists what moved.
3. Add an **integration** on the task (Slack, e-mail, Zapier, Make, webhook) triggered when a run
   succeeds, and read only the changed domains:

```
GET https://api.apify.com/v2/datasets/{datasetId}/items?view=changes&clean=true
```

With `onlyChanged` on, the dataset contains only the changes, so the notification can forward it
as is.

### Data sources and privacy (GDPR)

- **Sources**: RDAP servers of the registries, found through the IANA bootstrap file
  `https://data.iana.org/rdap/dns.json` (cached 24 hours), and the public DNS. No WHOIS, no
  scraping of registrar websites.
- **No personal data by design**: from RDAP, only the entity with the `registrar` role is read,
  and only its name and IANA ID. Registrant, administrative, technical, billing, reseller and abuse
  entities are skipped by the parser and never stored. The `RNAME` of the SOA record (a mailbox) is
  dropped.
- **E-mail addresses in DNS** (DMARC `rua`, CAA `iodef`, TXT records) are masked by default
  (`jane@corp.com` becomes `***@corp.com`); changes are still detected through a fingerprint of the
  original record.
- The state store contains only the comparable fields listed above, in your own Apify account.
- You remain responsible for the lawful use of the results, for example when you monitor domains
  that belong to individuals.

### Limits, honestly

- **Not every TLD publishes everything**. Some ccTLD registries publish no registrar or no
  expiration date over RDAP (`.de` publishes neither, for instance): those fields are `null`.
- **Some TLDs have no RDAP service** registered at IANA. They are reported as
  `rdap_unsupported` with DNS data only; the run does not fail. If a registry runs RDAP without
  being listed, use `rdapServerOverrides`.
- **Registry rate limits**: registries throttle RDAP. The Actor honours `Retry-After`, backs off
  and spaces its requests per server, but a very large list on a single TLD takes time, and a
  registry that keeps refusing gives `error` items (not charged).
- **DKIM cannot be proven absent**: selectors are arbitrary names. Only the selectors you list
  (and a built-in list of common ones) are tested.
- **DNS answers come from the resolver** used by the run (the platform resolver, or DNS over HTTPS
  as a fallback). Geo-dependent answers (CDNs) may differ from what your users see. Tracking `A` or
  `AAAA` changes behind a CDN is noisy, which is why only NS, MX, SPF and DMARC are tracked by
  default.
- **Registrable domain, not host**: `blog.example.com` is checked as `example.com`. Private
  suffixes of the Public Suffix List (such as `github.io`) are not treated as registries.
- **Delta mode needs the same store**: changing `stateStore` or `stateStoreName` starts a new baseline.
- **Permissions**: this Actor runs with limited permissions, which cannot open a store by name. Select
  a key-value store of your account in the `stateStore` field; the run is then allowed to read and
  write that store only. `stateStoreName` is used only when `stateStore` is empty and the Actor has
  full permissions. Without either, the run still succeeds, with delta mode turned off.
- This is a monitoring tool, not a legal proof of ownership.

### FAQ

**Why RDAP and not WHOIS?** RDAP is the standard that replaced WHOIS for gTLDs (ICANN retired the
WHOIS obligation in January 2025): structured JSON, documented error codes, and redaction built in.

**Can I check 50,000 domains?** Yes. The state store is sharded, concurrency is bounded and the
spending limit is respected. Plan for registry rate limits: a few domains per second per registry.

**What happens if a registry is down during a run?** Its domains are reported as `error` (not
charged) and their last known state is kept, so the next successful run compares with the right
baseline.

### Changelog

See [CHANGELOG.md](CHANGELOG.md).

# Changelog

This Actor's version history is a separate document: https://apify.com/rendangmedan/domain-monitor/changelog.md

# Actor input Schema

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

Domain names to check. URLs, upper case, sub-domains and internationalised names are accepted: each value is reduced to its registrable domain (https://www.Example.co.uk/page becomes example.co.uk). Duplicates are checked once.

## `deltaMode` (type: `boolean`):

Keep the last known state of every domain in a named key-value store and report what changed since the previous run: registrar, name servers, MX, SPF, DMARC, statuses, expiration date, DNSSEC.

## `onlyChanged` (type: `boolean`):

Write a dataset item only for domains with at least one change or alert, for domains seen for the first time (baseline) and for failures. Requires delta mode. Unchanged domains are still checked and counted as checked.

## `expiryAlertDays` (type: `integer`):

Raise an 'expiring_soon' change when a domain expires in this number of days or less. The alert is raised when the domain enters the window, then again at 14, 7, 3 and 1 days, not on every run. 0 disables the alert.

## `stateStore` (type: `string`):

Key-value store that holds the previous state for delta mode. Pick (or create) one in your account: this is how the Actor keeps its limited permissions. When empty, the store named in 'State store name' is used, which needs full permissions.

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

Name of the key-value store that holds the previous state. Use a different name per watch list if you want independent baselines. Letters, digits and hyphens only.

## `extraTrackedRecordTypes` (type: `array`):

NS and MX changes are always reported. Add record types here to be told about their changes as well. A and AAAA change often behind CDNs and load balancers: expect noise.

## `includeRdap` (type: `boolean`):

Registrar name and IANA ID, statuses, creation / update / expiration dates, registry name servers, DNSSEC delegation. Contact data (registrant, admin, tech) is never read.

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

Resolve the record types selected below.

## `includeEmailAuth` (type: `boolean`):

SPF (syntax, policy, DNS lookup count), DMARC (policy, alignment, reporting), DKIM for common selectors, MTA-STS, TLS-RPT and BIMI.

## `dnsRecordTypes` (type: `array`):

Record types resolved for each domain.

## `dkimSelectors` (type: `array`):

DKIM keys live under arbitrary selector names that cannot be listed. These selectors are probed; add the ones your mail providers use. Leave empty for the built-in list.

## `fetchMtaStsPolicy` (type: `boolean`):

When a domain publishes an MTA-STS record, download https://mta-sts.<domain>/.well-known/mta-sts.txt to read the mode and MX patterns.

## `redactEmailAddresses` (type: `boolean`):

Replace the local part of any e-mail address found in TXT / DMARC / CAA records by \*\*\* (jane@corp.com becomes \*\*\*@corp.com). Changes are still detected through a fingerprint of the original record. Keep this on unless you have a lawful basis to process these addresses.

## `maxConcurrency` (type: `integer`):

Global number of domains processed at the same time.

## `rdapMaxConcurrencyPerServer` (type: `integer`):

Maximum simultaneous requests sent to one RDAP server (one registry).

## `rdapMinDelayMs` (type: `integer`):

Spacing between two requests to the same RDAP server. 300 ms is about 3 requests per second per registry.

## `maxRetries` (type: `integer`):

Retries after a timeout, a 5xx or a 429 (Retry-After is honoured, exponential backoff otherwise).

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

Timeout of one RDAP or HTTPS request.

## `dnsTimeoutSecs` (type: `integer`):

Timeout of one DNS query.

## `dnsMode` (type: `string`):

auto: standard DNS, with DNS over HTTPS as a fallback when it fails. system: standard DNS only. doh: DNS over HTTPS only (Cloudflare, then Google).

## `dnsResolvers` (type: `array`):

IP addresses of the recursive resolvers to use for standard DNS, for example 1.1.1.1. Leave empty to use the resolver of the platform.

## `rdapServerOverrides` (type: `object`):

Advanced. Map a TLD to an RDAP base URL for registries that run RDAP without being listed in the IANA bootstrap registry, for example {"zz": "https://rdap.nic.zz/"}. HTTPS only.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "iana.org"
  ],
  "deltaMode": true,
  "onlyChanged": false,
  "expiryAlertDays": 30,
  "stateStoreName": "domain-monitor-state",
  "extraTrackedRecordTypes": [],
  "includeRdap": true,
  "includeDns": true,
  "includeEmailAuth": true,
  "dnsRecordTypes": [
    "A",
    "AAAA",
    "MX",
    "NS",
    "TXT",
    "CNAME",
    "CAA",
    "SOA"
  ],
  "dkimSelectors": [
    "default",
    "google",
    "selector1",
    "selector2",
    "k1",
    "k2",
    "s1",
    "s2",
    "dkim",
    "mail",
    "smtp",
    "mandrill",
    "fm1",
    "protonmail",
    "zoho",
    "mxvault"
  ],
  "fetchMtaStsPolicy": true,
  "redactEmailAddresses": true,
  "maxConcurrency": 10,
  "rdapMaxConcurrencyPerServer": 2,
  "rdapMinDelayMs": 300,
  "maxRetries": 3,
  "requestTimeoutSecs": 20,
  "dnsTimeoutSecs": 5,
  "dnsMode": "auto",
  "dnsResolvers": [],
  "rdapServerOverrides": {}
}
```

# Actor output Schema

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

One item per checked domain with registration data, DNS records, e-mail authentication audit and the changes since the previous run. Failed domains appear with status=error.

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

The same dataset, opened on the view that shows what changed since the previous run.

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

JSON record with counters: domains processed, statuses, billable domains, changed domains, change types.

# 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": [
        "example.com",
        "iana.org",
        "afnic.fr",
        "denic.de",
        "nic.io"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("rendangmedan/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": [
        "example.com",
        "iana.org",
        "afnic.fr",
        "denic.de",
        "nic.io",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("rendangmedan/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": [
    "example.com",
    "iana.org",
    "afnic.fr",
    "denic.de",
    "nic.io"
  ]
}' |
apify call rendangmedan/domain-monitor --silent --output-dataset

```

## MCP server setup

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