# Domain Watch: Expiry, DNS and Certificate Change Alerts (`accountable_eel/domain-watch-alerts`) Actor

Domain expiry and DNS change monitor: check when a list of domains expires, get alerts when A, MX, nameserver or TXT records change, plus an SSL certificate expiry monitor. One row for each domain. Scheduled: only domains with something new are returned and billed. To Google Sheets or n8n.

- **URL**: https://apify.com/accountable_eel/domain-watch-alerts.md
- **Developed by:** [Adriana Voss](https://apify.com/accountable_eel) (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.50 / 1,000 domain returneds

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

## Domain Watch: Expiry, DNS and Certificate Change Alerts

Give it a list of domains and get one row per domain: when the registration expires, who the
registrar is, the A, MX, NS and TXT records, and the SSL certificate the site serves today. Run the
same list on a schedule and each row also tells you **what changed since the last run**, so an
expiring domain, a swapped nameserver or a certificate that did not renew reaches you before your
customers notice. No API key, no account, no proxy.

### Who it's for

- **Agencies and freelancers** who look after client domains and do not want to learn about a lapsed
  registration from the client.
- **IT and operations teams** who need a daily answer to "did anything about our domains change?"
  without paying for a monitoring suite.
- **Security teams** watching for an unexpected nameserver, MX or TXT change, which is what a domain
  hijack or a mail redirection looks like from outside.
- **Domain investors and brand owners** tracking when names they care about come close to expiry or
  move into a redemption or pending-delete status.

### What you get

One flat row per domain:

| What | Columns |
| --- | --- |
| The verdict | `monitorStatus`, `hasChanges`, `changeCount`, `changeTypes`, `changeSummary`, `changes`, `alerts`, `previousCheckedAt` |
| Registration | `registeredDomain`, `registrar`, `registeredAt`, `expiresAt`, `daysUntilExpiry`, `domainStatus`, `registrationStatus` |
| DNS | `dnsA`, `dnsMx`, `dnsNs`, `dnsTxt`, `dnsStatus` |
| Certificate | `certIssuer`, `certIssuerName`, `certSubject`, `certValidFrom`, `certExpiresAt`, `certDaysUntilExpiry`, `certTrusted`, `certFingerprint`, `certificateStatus` |
| Provenance | `domain`, `sourceUrl`, `scrapedAt` |

Two example rows from a scheduled run, shortened:

| domain | monitorStatus | changeSummary | expiresAt | daysUntilExpiry | certIssuer | certDaysUntilExpiry |
| --- | --- | --- | --- | --- | --- | --- |
| example.com | CHANGED | MX records changed: added 10 mail.example.com; removed 0 .. | 2027-08-13T04:00:00Z | 305 | SSL Corporation | 75 |
| example.org | CHANGED | Domain registration expires in 19 days, on 2026-10-31. | 2026-10-31T00:00:00Z | 19 | Let's Encrypt | 41 |

`changes` carries the detail behind the sentence: for every change its type, the field, the previous
value and the current value.

#### What counts as a change

| `changeTypes` value | Fires when |
| --- | --- |
| `domain-expiring`, `domain-expired` | the registration is inside your warning window, in its last 7 days, or past its expiry date |
| `certificate-expiring`, `certificate-expired` | the same, for the certificate the site serves |
| `domain-expiry-changed` | the expiry date moved, which is what a renewal looks like |
| `registrar-changed` | the domain was transferred to another registrar |
| `domain-status-changed` | the registry status list changed, for example to `pending delete` |
| `dns-a-changed`, `dns-mx-changed`, `dns-ns-changed`, `dns-txt-changed` | a record was added or removed |
| `certificate-issuer-changed` | the certificate now comes from a different authority |
| `certificate-renewed` | the served certificate has a later expiry date than before |

Expiry warnings need no earlier run: a domain that expires in 12 days says so the first time you
check it. Each warning is reported once when it starts and once more when the last 7 days begin, not
every day in between. `alerts` lists every warning that is active right now on every run.

### How change tracking works

- **A plain run** (the default) returns every domain with its current state and its active expiry
  warnings. The change columns are empty because nothing is remembered.
- **Turn on "Only return domains with something new"** and the run remembers each domain. The first
  run returns every domain as `BASELINE`, so you can see what was recorded. From then on a domain is
  returned only when something changed or a new warning fired. A quiet domain is not returned and
  not charged.
- **Give the watchlist a name** to keep two lists apart, or to track changes while still getting
  every domain back on every run (`NO_CHANGE` rows included).

Three rules keep the alerts honest:

1. **An outage is not a change.** If a registry, a resolver or the site itself does not answer on
   one run, that part of the remembered state is left as it was and nothing is reported. The next
   run that does get an answer compares against the last value actually seen.
2. **Order and timing are not changes.** Records are compared as sorted sets, and TTLs are ignored.
3. **Rotating IP addresses can be switched off.** A site behind a CDN or load balancer may hand out
   a different A record on every lookup. Untick "Watch A records" for such a list and only MX, NS
   and TXT changes are reported.

### How to use Domain Watch

1. **In the Apify Console.** Open the actor page and click **Start** — the `domains` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~domain-watch-alerts/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"domains":["example.com","github.com"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

### Input

```json
{
  "domains": [
    "example.com",
    "github.com"
  ]
}
```

One domain per line. Each one is checked for its registration and expiry date, its DNS records and the certificate it serves. Run the same list on a schedule and every row tells you what changed since the run before. Accepted formats: example.com, https://www.github.com, api.example.com.

### Output

One row per domain, for example:

| query | found | status | domain | sourceUrl | monitorStatus | hasChanges | changeCount | changeTypes | changeSummary | changes | alerts | previousCheckedAt | registeredDomain | registrar | registeredAt | expiresAt | daysUntilExpiry | domainStatus | registrationStatus | dnsA | dnsMx | dnsNs | dnsTxt | dnsStatus | certIssuer | certIssuerName | certSubject | certValidFrom | certExpiresAt | certDaysUntilExpiry | certTrusted | certFingerprint | certificateStatus | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| example.com | true | OK | <domain> | <source url> | <monitoring status> | <changed since last run> | <number of changes> | <what changed> | <changes in plain words> | \<changes (full detail)> | <expiry warnings active now> | <previous check> | <registered domain> | <registrar> | <registered on> | <registration expires on> | <days until registration expires> | <registry status> | <registration check> | <a records> | <mx records> | <ns records> | <txt records> | <dns check> | <certificate issuer> | <issuing certificate> | <certificate issued to> | <certificate valid from> | <certificate expires on> | <days until certificate expires> | <certificate trusted> | \<certificate fingerprint (sha-256)> | <certificate check> | 1970-01-01T00:00:00.000Z |

A name that is not registered and does not resolve comes back as a row with `"found": false` and a
`message` that says so. It is never charged.

### Example runs

Three inputs that need no credentials of any kind and return rows as they stand. Paste one into the
Input tab and press Start.

**1. Where do these domains stand today?**

```json
{
  "domains": ["example.com", "github.com"],
  "expiryWarnDays": 60
}
```

**2. A daily watch that only returns what is new**

Save this as a task and put it on a daily schedule. The first run returns both domains as the
baseline; later runs return a domain only when something changed.

```json
{
  "domains": ["example.com", "github.com"],
  "onlyNewSinceLastRun": true,
  "deltaName": "my-domains",
  "expiryWarnDays": 30,
  "certExpiryWarnDays": 14
}
```

**3. A mail and nameserver watch for a domain behind a CDN**

```json
{
  "domains": ["github.com"],
  "onlyNewSinceLastRun": true,
  "deltaName": "mail-and-ns",
  "watchARecords": false
}
```

### Pricing

Pay-per-event. A flat per-run fee covers session/proxy warmup; each independent
source is billed under its own event, only when that source actually returns
data — see `.actor/pay_per_event.json` for exact prices. A miss is never
charged.

You pay once per domain row returned, and nothing else:

- A domain that is not registered and does not resolve is free.
- A domain no check could read on this run is free.
- With "Only return domains with something new" on, a domain where nothing changed is not returned
  and is free. A watchlist of 200 quiet domains costs the start fee and nothing more.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~domain-watch-alerts/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"domains":["example.com","github.com"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~domain-watch-alerts/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"domains":["example.com","github.com"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~domain-watch-alerts/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"domains":["{{domain}}"]}`, mapping the row's domain into the `domains` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "domain expiry and DNS change monitor" — the agent will find and run this actor.

### Where the data comes from

- **Registration and expiry** come from RDAP, the registries' own machine-readable successor to
  WHOIS. Each domain is looked up at the registry that runs its ending.
- **DNS records** come from two public DNS-over-HTTPS resolvers, the second used when the first does
  not answer.
- **The certificate** is read by connecting to the domain on port 443 the way a browser does. It is
  the certificate the site is serving right now, which is the one whose expiry can take it down.

### Limits, stated plainly

- **Many country domains publish no RDAP data.** For those the registration columns are empty and
  `registrationStatus` says `NO_RDAP_SERVER`. DNS and certificate watching work for every domain.
- **Expiry dates are the registry's.** A registrar may keep a domain alive for a grace period after
  the date shown, and a registry may show a date one year ahead during auto-renewal.
- **One certificate per check.** A large site may serve different certificates from different
  servers. Only a certificate with a later expiry date than the remembered one is reported as a
  renewal, so those sites do not produce a stream of alerts.
- **A host with no HTTPS** has empty certificate columns and `certificateStatus: NO_TLS`.
- **Changes are detected between runs.** A record that changed and changed back between two runs is
  not seen. Run more often if that matters.
- **No registrant or contact details.** This actor returns nothing about who owns a domain.

### Data & privacy

Everything returned is public infrastructure data: registry records, DNS records and the
certificate a server presents to every visitor. The actor reads no registrant, administrative or
technical contact, so it returns no personal data. The state it remembers between runs is stored in
a key-value store in your own Apify account, under the watchlist name you chose.

### FAQ

**Why did my first scheduled run return every domain?**
The first run of a domain is its baseline. It is returned so you can see what was recorded. From the
second run on, only domains with something new are returned.

**Why is `expiresAt` empty for my domain?**
Its registry publishes no RDAP data. This is common for country endings. The DNS and certificate
columns are still filled and still watched.

**Can I watch a host such as `api.example.com`?**
Yes. DNS and the certificate are checked for the host you gave; registration is looked up for the
registered name above it, shown in `registeredDomain`.

**Will I get the same expiry warning every day?**
No. A warning is reported when it starts and again when the last 7 days begin. `alerts` still lists
it on every row while it is active.

**Do I need a proxy?**
No.

### Related actors

- [WHOIS & RDAP Lookup: Bulk Domain Age, Expiry & Registrar](https://apify.com/accountable_eel/domain-rdap-lookup): bulk domain age, expiry and registrar in one run.

- **Domain RDAP Lookup** for a one-off registration record.

- **DNS Record Lookup** for one record type with email-security columns.

- **TLS Certificate Lookup** for the history of certificates issued for a domain.

- **Domain Availability Check** for whether a name is free to register.

# Actor input Schema

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

One domain per line. Each one is checked for its registration and expiry date, its DNS records and the certificate it serves. Run the same list on a schedule and every row tells you what changed since the run before. Accepted formats: example.com, https://www.github.com, api.example.com. You're only charged for the ones we actually find — a miss costs nothing.

## `expiryWarnDays` (type: `integer`):

A domain whose registration expires within this many days gets a domain-expiring warning, on the very first run too. 0 turns the warning off.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

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

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `certExpiryWarnDays` (type: `integer`):

A certificate that expires within this many days gets a certificate-expiring warning. Most certificates renew automatically about 30 days before they expire, so 14 means a renewal that did not happen. 0 turns the warning off.

## `onlyNewSinceLastRun` (type: `boolean`):

Turn this on for a scheduled run. A domain where nothing changed and no new warning fired is not returned and not charged. The first run of a domain is always returned, so you can see what was recorded.

## `deltaName` (type: `string`):

Optional. Name your watchlist, for example "client-domains". Each name keeps its own memory, so two schedules with different names never mix. Giving a name also switches change tracking on while still returning every domain on every run.

## `watchARecords` (type: `boolean`):

On by default. Turn it off for domains behind a CDN or load balancer that hands out a different IP address on every lookup, so rotating addresses are not reported as changes. MX, NS and TXT records are always watched.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

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

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "github.com"
  ],
  "expiryWarnDays": 30,
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "certExpiryWarnDays": 14,
  "onlyNewSinceLastRun": false,
  "deltaName": "",
  "watchARecords": true,
  "columns": [
    "domain",
    "sourceUrl",
    "monitorStatus",
    "hasChanges",
    "changeCount",
    "changeTypes",
    "changeSummary",
    "changes",
    "alerts",
    "previousCheckedAt",
    "registeredDomain",
    "registrar",
    "registeredAt",
    "expiresAt",
    "daysUntilExpiry",
    "domainStatus",
    "registrationStatus",
    "dnsA",
    "dnsMx",
    "dnsNs",
    "dnsTxt",
    "dnsStatus",
    "certIssuer",
    "certIssuerName",
    "certSubject",
    "certValidFrom",
    "certExpiresAt",
    "certDaysUntilExpiry",
    "certTrusted",
    "certFingerprint",
    "certificateStatus"
  ],
  "maxConcurrency": 5
}
```

# Actor output Schema

## `results` (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": [
        "example.com",
        "github.com"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/domain-watch-alerts").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",
        "github.com",
    ],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/domain-watch-alerts").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",
    "github.com"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/domain-watch-alerts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/domain-watch-alerts"
        }
    }
}
```

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/Sk5WUl10StBncLlLV/builds/NctvmWHbA0OliNezz/openapi.json
