# Bulk Domain Expiry, SSL & DNS Checker (`echolab/domain-health-monitor`) Actor

Bulk domain checker: input a list of up to 5,000 domains; output one row per domain with expiration date and days left (RDAP), registrar, SSL certificate validity and expiry, DNS records, SPF/DMARC, and warning codes like domain_expires_soon. No API keys or login.

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

## Pricing

from $1.50 / 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.

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

## Bulk Domain Expiry, SSL & DNS Checker

**Bulk domain expiry checker and SSL certificate expiry checker** in one run: paste up to 5,000 domains or URLs and get one report per host with the domain expiration date and registrar (from RDAP, the modern WHOIS replacement), SSL certificate validity and days left, DNS records (A, AAAA, MX, NS, TXT, CAA) and SPF / DMARC email security. No API keys, browsers or proxies. Schedule it daily and send the warnings to Slack, email or a webhook so no domain or certificate expires by surprise.

- **Domain expiration date** and days left, registrar, registration date, status, nameservers and DNSSEC via RDAP
- **SSL certificate check**: chain and hostname verification, expiry date and days left, issuer, SANs, TLS version
- **Bulk DNS lookup** over DNS-over-HTTPS, plus **SPF and DMARC** policy checks
- Short warning codes (`domain_expires_soon`, `ssl_expired`, `no_dmarc` …) ready for alerts

### What can I use this bulk domain checker for?

- **Agencies and freelancers**: audit every client domain for upcoming domain renewals and SSL expiry in one table.
- **IT teams and MSPs**: schedule daily certificate and renewal checks and alert on the warning codes.
- **Email deliverability**: find domains without SPF, without DMARC or with `p=none`.
- **Domain portfolios and SEO**: see registrar, expiry and DNS for hundreds of domains at once, and spot names that are not registered (`rdapError: domain_not_found`).

### How much does it cost to check domains in bulk?

This Actor is priced **per result**: **$1.50 per 1,000 domains** saved to the dataset ($0.0015 per domain), plus an Actor start fee of $0.00005 per run. Apify platform usage is included in that price, so you do not pay separately for compute.

| Example run | Domains | Cost |
|---|---|---|
| Daily check of 20 client domains | 20 | about $0.03 per run, about $0.90 per month if run daily |
| One-off audit of 1,000 domains | 1,000 | about $1.50 |
| Maximum batch | 5,000 | about $7.50 |

Duplicates and invalid lines are skipped and not billed. Set a maximum cost per run in the run options: the Actor stops cleanly when the limit is reached. The price shown on the Pricing tab of this page is the one that applies.

### Input

| Field | Default | Description |
|---|---|---|
| domains | Required | 1–5,000 strings: bare domains or HTTP(S) URLs |
| checkRdap | true | Registration data from RDAP |
| checkSsl | true | Verified TLS connection on port 443 |
| checkDns | true | Query selected DNS records |
| dnsRecordTypes | A, AAAA, MX, NS, TXT, CAA | Also supports CNAME, SOA, PTR, SRV |
| checkEmailSecurity | true | SPF at apex and DMARC at `_dmarc` |
| expiryWarningDays | 30 | 1–365 days |
| maxConcurrency | 5 | 1–20 concurrent domains |
| timeoutSecs | 15 | 1–120 seconds per request/connection attempt |

```json
{"domains":["apify.com","https://www.example.com/page"],"expiryWarningDays":30,"maxConcurrency":5}
```

Hosts are lowercased, IDNA-encoded, stripped of scheme/path/port and deduplicated by host. The first original value is retained. SSL/DNS use that host (including `www`); RDAP and email security use its registrable apex via the bundled public suffix list (registry rules, not private hosting suffixes). For example, `expired.badssl.com` checks registration of `badssl.com`. Values that are not valid domains or HTTP(S) URLs are skipped (and listed in the run log) without being billed; the run fails only if no valid domain remains.

### Output

One dataset item per unique host, pushed as soon as it is ready. Full live examples are in `tests/sample_output.json`.

```json
{"domain":"example.com","inputValue":"example.com","checkedAt":"2026-11-04T07:23:09Z","registrar":"RESERVED-Internet Assigned Numbers Authority","expirationDate":null,"daysUntilDomainExpiry":null,"sslValid":true,"sslError":null,"sslNotAfter":"2026-12-25T22:56:35Z","hasSpf":true,"hasDmarc":true,"dmarcPolicy":"reject","warnings":[],"status":"ok"}
```

Example above is abbreviated; every actual item contains all stable keys below (unknown or disabled values are null).

| Fields | Meaning |
|---|---|
| domain, inputValue, checkedAt | Normalized checked host, first input, UTC timestamp |
| registrar | Registrar company only; no contact entities |
| registrationDate, expirationDate, lastChangedDate | UTC RDAP events |
| daysUntilDomainExpiry | Floor of remaining days; null if expiry is unavailable |
| domainStatus, nameservers, dnssecSigned, rdapError | RDAP statuses, lowercase servers, delegation signing, retrieval error: `no_rdap_server` (the TLD has no public RDAP service), `domain_not_found` (the registry has no such domain, so it is not registered), `rdap_failed: …` |
| sslValid, sslError | Chain/hostname verified; false on certificate rejection, null on connection failure |
| sslIssuer, sslSubject, sslSans | Issuer O/CN, subject CN, DNS SANs capped at 100 |
| sslNotBefore, sslNotAfter, daysUntilSslExpiry, sslTlsVersion | Certificate dates, remaining days and negotiated TLS |
| dns | Type → answer list; empty means no records, null type means failed query |
| hasSpf, spfRecord | Apex SPF presence and first record |
| hasDmarc, dmarcPolicy, dmarcRecord | DMARC presence, recognized policy and first record |
| warnings | domain_expired, domain_expires_soon, domain_expiry_unknown, ssl_expired, ssl_expires_soon, ssl_invalid, no_mx, no_spf, no_dmarc, dmarc_policy_none |
| status | ok, warning, or error (retrieval failures); warnings still retained on error |
| error, errorMessage | Optional short per-input DNS/email/unexpected error; other domains continue |

Mailbox addresses in TXT/DMARC and diagnostic strings are redacted as `[redacted-email]`. Records are otherwise preserved. Disabled checks remain null, including dependent expiry counters. `no_mx` is emitted only when MX was requested and returned no answers.

### Billing details

One unique host is one billed result, including a useful partial/error report. Duplicates are not billed again, and invalid input lines are skipped for free. Set a maximum cost per run in the run options: the Actor stops normally when that limit is reached; in-flight checks may complete without being pushed.

### Tips / limits

- Retry public endpoints up to three attempts with exponential backoff and respect Retry-After on 429/5xx. Google DNS is the fallback for failed Cloudflare requests.
- **Which TLDs have expiry dates?** All gTLDs (.com, .net, .org, .app, .dev, .xyz, .shop and other new gTLDs) plus the ccTLDs that publish RDAP, such as .uk, .fr, .ca, .in, .br, .kr, .tv, .ai. Popular ccTLDs missing from the IANA list are covered by their registries' official RDAP services: .io, .co, .me, .so, .sh, .ac, .ag, .bz, .lc, .mn, .vc, .gi.
- Some ccTLDs are not in the IANA RDAP list and have no public RDAP service we could verify (in November 2026 for example .de, .jp, .cn, .ru, .eu, .ch, .it, .es, .se, .dk, .at, .be, .us, .edu): `rdapError` is `no_rdap_server` and the status is `error`, while SSL, DNS and email checks still run. No WHOIS fallback is used.
- Some registries answer RDAP but do not publish an expiry date (for example .nl, .au, .pl): the item gets the `domain_expiry_unknown` warning instead of a silent `ok`. RDAP expiry is the registry date, which can differ from the renewal date shown by your registrar.
- Non-verifying TLS reconnect is used **only** after certificate verification rejection to obtain certificate dates; it never changes `sslValid` to true. Network failures may prevent certificate capture.
- SPF/DMARC checks detect records, not full SPF expansion, mail delivery, syntax compliance or effective organizational inheritance. Null MX (`0 .`) is preserved; multiple SPF/DMARC records require manual review.
- DNS records and certificate metadata are public infrastructure data. Do not submit URLs containing credentials or personal information. Hostnames are intended to be public; this is not a port scanner.

### FAQ

#### Is this a WHOIS lookup?

It uses RDAP, the registry-run successor to WHOIS. RDAP returns structured registration and expiry dates, so nothing is scraped from WHOIS text. Contact details of registrants are never output, only the registrar company name.

#### How many domains can I check in one run?

Up to 5,000 domains or URLs. Hosts are normalized (scheme, path and port removed) and deduplicated; invalid lines are skipped without being billed.

#### Can I get alerts before a domain or SSL certificate expires?

Yes. Schedule the Actor daily in Apify, set `expiryWarningDays` (default 30), and filter rows whose `warnings` contain `domain_expires_soon`, `ssl_expires_soon`, `domain_expired` or `ssl_expired`. Send them to Slack, email or a webhook with Apify integrations.

#### Why is the status `error` for my .de / .jp domain?

Those registries are not in the IANA RDAP list and we found no public RDAP service for them, so the registration part cannot be checked (`rdapError: no_rdap_server`). The SSL, DNS and SPF/DMARC results in the same row are still valid.

#### Can I check only SSL, only DNS or only SPF/DMARC?

Yes. Turn the other checks off (`checkRdap`, `checkSsl`, `checkDns`, `checkEmailSecurity`); disabled fields stay null and the run is faster.

#### Can an AI agent call it?

Yes. It is a regular Apify Actor, so agents can run it through the Apify API or Apify's MCP server with an input like `{"domains": ["example.com"]}`.

### Compliance & data source

Only official public endpoints: [IANA RDAP bootstrap](https://data.iana.org/rdap/dns.json), registry RDAP services it advertises (plus the official registry RDAP services listed above for ccTLDs missing from it), [Cloudflare DoH](https://cloudflare-dns.com/dns-query), [Google DNS](https://dns.google/resolve), and direct TLS on the supplied host. Descriptive Echo Lab User-Agent, bounded concurrency, no login/cookies/CAPTCHA bypass. Registrant/admin/technical contact entities and their vcards are never output; only the registrar company name is retained. Mailbox addresses are redacted. Users are responsible for complying with source terms and monitoring domains they are authorized to check.

### Local development

```sh
UV_CACHE_DIR=/tmp/uvc uv pip install -p /tmp/apy/.venv -r requirements.txt pytest
/tmp/apy/.venv/bin/python -m pytest -q
APIFY_LOCAL_STORAGE_DIR=./storage /tmp/apy/.venv/bin/python -m src
```

Place input in `storage/key_value_stores/default/INPUT.json`. Storage is ignored by Git and Docker. Fixtures are minimized official responses with synthetic substitutions for deterministic assertions and contact-leak regression tests; certificate tests generate an expired DER certificate offline.

### Support

Open an issue on the Actor's Issues tab.

More real output, worked pricing examples and a comparison with alternatives: https://echolab-tools.pages.dev/bulk-domain-expiry-ssl-checker/

# Actor input Schema

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

Domains or HTTP(S) URLs to check, 1 to 5,000 per run, e.g. example.com or https://www.example.com/page. Scheme, path and port are removed and hosts are deduplicated. SSL and DNS use the host as given (including www); registration (RDAP) and SPF/DMARC use the registrable domain.

## `checkRdap` (type: `boolean`):

Look up registrar, registration date, expiration date and days until expiry from the registry's RDAP service (the WHOIS replacement). Off = these fields are null.

## `checkSsl` (type: `boolean`):

Connect on port 443 and verify the SSL/TLS certificate: valid or not, issuer, expiry date and days left. Off = these fields are null.

## `checkDns` (type: `boolean`):

Look up the DNS record types listed below over DNS-over-HTTPS (Cloudflare, Google as fallback). Off = dns is null.

## `checkEmailSecurity` (type: `boolean`):

Check the SPF record at the domain and the DMARC record at \_dmarc.<domain>, including the DMARC policy. Off = these fields are null.

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

Supported: A, AAAA, MX, NS, TXT, CAA, CNAME, SOA, PTR, SRV.

## `expiryWarningDays` (type: `integer`):

Add domain_expires_soon / ssl_expires_soon warnings when the domain or certificate expires within this many days (1 to 365).

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

How many domains are checked in parallel (1 to 20).

## `timeoutSecs` (type: `integer`):

Timeout for each HTTP request or TLS connection attempt, in seconds (1 to 120).

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "example.com",
    "wikipedia.org"
  ],
  "checkRdap": true,
  "checkSsl": true,
  "checkDns": true,
  "checkEmailSecurity": true,
  "dnsRecordTypes": [
    "A",
    "AAAA",
    "MX",
    "NS",
    "TXT",
    "CAA"
  ],
  "expiryWarningDays": 30,
  "maxConcurrency": 5,
  "timeoutSecs": 15
}
```

# Actor output Schema

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

Dataset items, one per result row.

# 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": [
        "apify.com",
        "example.com",
        "wikipedia.org"
    ],
    "checkRdap": true,
    "checkSsl": true,
    "checkDns": true,
    "checkEmailSecurity": true,
    "dnsRecordTypes": [
        "A",
        "AAAA",
        "MX",
        "NS",
        "TXT",
        "CAA"
    ],
    "expiryWarningDays": 30,
    "maxConcurrency": 5,
    "timeoutSecs": 15
};

// Run the Actor and wait for it to finish
const run = await client.actor("echolab/domain-health-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": [
        "apify.com",
        "example.com",
        "wikipedia.org",
    ],
    "checkRdap": True,
    "checkSsl": True,
    "checkDns": True,
    "checkEmailSecurity": True,
    "dnsRecordTypes": [
        "A",
        "AAAA",
        "MX",
        "NS",
        "TXT",
        "CAA",
    ],
    "expiryWarningDays": 30,
    "maxConcurrency": 5,
    "timeoutSecs": 15,
}

# Run the Actor and wait for it to finish
run = client.actor("echolab/domain-health-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": [
    "apify.com",
    "example.com",
    "wikipedia.org"
  ],
  "checkRdap": true,
  "checkSsl": true,
  "checkDns": true,
  "checkEmailSecurity": true,
  "dnsRecordTypes": [
    "A",
    "AAAA",
    "MX",
    "NS",
    "TXT",
    "CAA"
  ],
  "expiryWarningDays": 30,
  "maxConcurrency": 5,
  "timeoutSecs": 15
}' |
apify call echolab/domain-health-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,echolab/domain-health-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/2fMufMpfLhbsNCHai/builds/Zb9kNzc8umHhS2x7b/openapi.json
