# Domain WHOIS, DNS & SSL Checker: Expiry, SPF & DMARC (`private_image_hg9/domain-whois-dns-ssl-checker`) Actor

Check domains in bulk: registration and expiry (RDAP), DNS records, SPF, DMARC and DKIM, the SSL certificate and HTTPS redirects. One row per domain with a list of issues found. Pay only for domains that answered.

- **URL**: https://apify.com/private_image_hg9/domain-whois-dns-ssl-checker.md
- **Developed by:** [Michael Shanker](https://apify.com/private_image_hg9) (community)
- **Categories:** Developer tools, SEO tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 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 WHOIS, DNS & SSL Checker: Expiry, SPF & DMARC

Give it a list of domains and get back **one row per domain** with everything you'd otherwise look up in five different tools: **registration and expiry** (registrar, created, expires, status codes, DNSSEC), **DNS records**, **email authentication** (SPF, DMARC, DKIM, MTA-STS, BIMI), the **SSL certificate** and **HTTPS redirects**, plus a plain-English **list of issues** such as "expires in 12 days", "no DMARC record" or "certificate is for another name".

**You pay only for domains where at least one check returned data.** A "not registered" answer counts as data, since that's often exactly what you want to know. Entries that aren't domains, and domains where nothing answered at all, still get a row but cost nothing.

### Who it's for

- **Agencies and IT teams** watching a portfolio of client or company domains for expiring registrations and certificates.
- **Email deliverability and security** work: find domains with no DMARC, a weak SPF (`+all`, `?all`), too many SPF lookups, or missing DKIM.
- **Sales and research**: check lead or vendor domains in bulk (is it registered, who's the registrar, does the website work, is email set up).
- **Domain investors**: registration dates, expiry and status codes (on hold, redemption period, pending delete) for a list of names.

### What makes it different

| | This tool | A typical WHOIS, DNS or SSL tool in the Store |
|---|---|---|
| Checks in one row | Registration, DNS, SPF/DMARC/DKIM, SSL, HTTP | Usually one of these |
| Registration data | RDAP, the registries' official structured service, with dates in one format | Often parsed WHOIS text, which differs per registry |
| Issues list | Yes: code, severity and a plain message per problem | Usually raw data only |
| Input | Domains, hosts, web addresses or email addresses | Usually bare domains |
| Personal data | Registrant, admin and tech contacts are always dropped | Varies |
| Price | $1.50 per 1,000 domains, only for domains that answered | About $1 to $3 per 1,000 |

### What each check does

- **Registration (RDAP).** Finds each TLD's official RDAP server in the IANA list (rdap.org for TLDs not listed) and reads: registered or not, registrar name and IANA id, created, expires and last-changed dates, days until expiry, status codes (like `client transfer prohibited` or `redemption period`), nameservers and whether DNSSEC is signed. For TLDs with no public RDAP service (some country domains), "registered or not" is read from DNS instead and `source` says `dns`.
- **DNS.** A, AAAA, NS, MX, TXT, CAA (from the host up to the domain), SOA serial and primary nameserver, the host's CNAME and the CNAME of `www`. Uses the normal resolver, and switches to DNS over HTTPS (Cloudflare, then Google) once per run if plain DNS is blocked.
- **Email authentication (DNS only, no mail server is ever contacted).** SPF record and its `all` ending, includes, the total DNS lookups it needs with nested includes followed (over 10 breaks SPF), and multiple records. DMARC policy (`p`, `sp`, `pct`), whether reports are requested and to which domains, found on the host or inherited from the main domain. DKIM keys at common selectors (google, selector1, selector2, k1, k2, s1, s2, default, dkim, mail) plus any you add. MX, null MX, MTA-STS and BIMI presence.
- **SSL.** A TLS handshake on port 443 with the host's name (10 second timeout): certificate name and alternative names, issuer, valid from and to, days left, whether the name matches, whether the chain is trusted (and why not), and the TLS version negotiated.
- **HTTP.** Requests `https://host/` and `http://host/`, follows up to 10 redirects by hand, and reports the final address and status, the HSTS header and whether http moves to https. Only status codes and headers are read.

### Input

| Field | What it does |
|---|---|
| **Domains** | One per line: `acme.com`, `shop.acme.co.uk`, `https://www.acme.com/about` or `jane@acme.com`. Registration is looked up for the registrable domain (`acme.co.uk`); the other checks use the host as given. Repeats are checked once. |
| **Checks** | Any of registration, dns, email, ssl, http. All on by default. |
| **Extra DKIM selectors** | Selectors to check on top of the common ones, e.g. the `s=` value from a DKIM-Signature header. |
| **Domains at once** | Parallel domains (default 10, max 50). |

```json
{
  "domains": ["acme.com", "https://www.acme.co.uk/shop", "billing@acme.de"],
  "checks": ["registration", "dns", "email", "ssl", "http"],
  "dkimSelectors": ["mandrill"]
}
```

### Output

One row per domain (shortened, values illustrative):

```json
{
  "input": "https://acme.com/about",
  "domain": "acme.com",
  "host": "acme.com",
  "status": "ok",
  "registration": {
    "registered": true, "source": "rdap", "registrar": "Example Registrar, Inc.", "registrarIanaId": "1234",
    "createdAt": "2012-03-14T17:02:11Z", "expiresAt": "2026-11-24T17:02:11Z", "updatedAt": "2025-10-25T09:13:40Z",
    "daysUntilExpiry": 20, "statuses": ["client transfer prohibited"],
    "nameservers": ["ns1.dnshost.net", "ns2.dnshost.net"], "dnssecSigned": false, "rdapServer": "rdap.verisign.com"
  },
  "dns": {
    "a": ["93.184.215.14"], "aaaa": [], "ns": ["ns1.dnshost.net", "ns2.dnshost.net"],
    "mx": [{"priority": 10, "host": "mx1.mailhost.com"}], "txt": ["v=spf1 include:_spf.mailhost.com ~all"],
    "caa": [{"flags": 0, "tag": "issue", "value": "letsencrypt.org"}], "soaSerial": 2025110301, "wwwCname": "acme.com"
  },
  "email": {
    "mxPresent": true,
    "spf": {"present": true, "all": "~all", "includeCount": 1, "dnsLookups": 4, "tooManyLookups": false},
    "dmarc": {"present": false, "policy": null},
    "dkim": {"found": ["google"]},
    "mtaSts": false, "bimi": false
  },
  "ssl": {
    "host": "acme.com", "tlsVersion": "TLSv1.3", "subjectCN": "acme.com", "sans": ["acme.com", "www.acme.com"],
    "issuerOrg": "Let's Encrypt", "notAfter": "2026-11-12T08:00:00Z", "daysLeft": 8,
    "hostnameMatches": true, "chainValid": true
  },
  "http": {
    "finalUrl": "https://www.acme.com/", "finalStatus": 200, "hsts": null, "httpToHttps": true
  },
  "issues": [
    {"code": "domain_expires_soon", "severity": "warning", "message": "acme.com expires in 20 days (2026-11-24)."},
    {"code": "dmarc_missing", "severity": "warning", "message": "No DMARC record at _dmarc.acme.com: spoofed mail is not blocked."},
    {"code": "ssl_expires_soon", "severity": "warning", "message": "The SSL certificate of acme.com expires in 8 days."},
    {"code": "no_hsts", "severity": "info", "message": "https://www.acme.com/ sends no Strict-Transport-Security (HSTS) header."}
  ],
  "issueCount": 4,
  "worstSeverity": "warning",
  "error": null,
  "checkedAt": "2026-11-04T10:12:01+00:00"
}
```

`status` is one of:

- `ok`: at least one check returned data. Charged.
- `not_registered`: the registry says the domain doesn't exist (or, for TLDs without RDAP, it isn't in the TLD's DNS zone). Charged. The other checks are skipped.
- `invalid_domain`: the entry isn't a usable public domain (an IP address, `localhost`, `co.uk` on its own, a made-up TLD). Free.
- `error`: nothing answered (`error` says what failed). Free.

**Issue codes** (severity in brackets): `domain_expired` (critical), `domain_expires_soon` under 30 days (warning), `domain_status_client_hold`, `_server_hold`, `_redemption_period`, `_pending_delete` (critical), `domain_not_in_dns` (critical), `not_registered` (critical), `dnssec_unsigned` (info), `host_not_found` (critical), `no_address`, `single_nameserver` (warning), `no_caa` (info), `spf_missing` (warning, info if the domain gets no mail), `spf_plus_all`, `spf_multiple_records` (critical), `spf_neutral_all`, `spf_too_many_lookups` (warning), `spf_no_all` (info), `dmarc_missing` (warning), `dmarc_multiple_records` (critical), `dmarc_invalid` (warning), `dmarc_policy_none`, `dmarc_no_reports`, `dkim_not_found`, `no_mx` (info), `ssl_expired`, `ssl_not_yet_valid`, `ssl_hostname_mismatch`, `ssl_chain_invalid` (critical), `ssl_expires_soon` under 14 days, `ssl_unavailable`, `tls_outdated` (warning), `https_not_working` (critical), `https_redirect_problem`, `https_error_status`, `no_https_redirect`, `private_address` (warning), `no_hsts` (info).

The dataset has three views: an **Overview** table, an **Issues** table and a **Registration** table. Download as JSON, CSV or Excel, or send results to Google Sheets, Zapier, Make or n8n. The run also saves a `SUMMARY` record with counts per status, issues by severity, and what was charged.

### Pricing

Pay per event: **$0.0015 per domain checked ($1.50 per 1,000)**, for every row with status `ok` or `not_registered`. No start fee, and the price is the same however many checks you turn on. Invalid entries and domains where nothing answered are free. Set a maximum charge per run in the run options and the Actor stops cleanly when it's reached.

### Good to know

- **RDAP coverage.** Nearly all generic TLDs (.com, .net, .org, .io, .app, .dev...) and many country TLDs have RDAP. A few country registries don't offer public RDAP; for those you get registered or not and the nameservers from DNS, but no registrar or dates. Some registries also leave out or redact fields.
- **Registries limit request rates.** At most 2 requests at a time go to one RDAP server, and a "too many requests" answer pauses that server before trying again. Very large lists on one TLD take longer.
- **DKIM can only be checked by name.** There is no way to list a domain's DKIM keys, so "not found" means none of the selectors checked exist, not that the domain has no DKIM.
- **SSL and HTTP use the host you give.** For a bare domain with no address of its own, `www.` is checked instead (the `ssl.host` field says which). Hosts that resolve to private or internal addresses are never contacted.
- **robots.txt is respected.** If a site's robots.txt asks tools to stay off its home page, the HTTP check is skipped for it (`http.skipped` says so). The SSL check is a TLS handshake only.
- No mail server is ever contacted. A domain's own servers get only the TLS handshake, its robots.txt, and the home page over https and http (plus any redirects), read for status and headers only.

### Responsible use

- Only public registration facts are kept. **Registrant, admin, tech, billing and abuse contacts, and every vCard in the RDAP answer, are dropped** before anything is saved, so no names, emails, phone numbers or postal addresses of people end up in the output. DMARC report addresses are reduced to their domain.
- Requests name the tool in their User-Agent (`DomainHealthChecker`).

This Actor isn't affiliated with or endorsed by IANA, any registry, registrar, DNS provider or certificate authority.

### More tools from the same developer

- [Broken Link Checker: 404s, Redirects & Dead Links](https://apify.com/private_image_hg9/broken-link-checker)
- [SEO Audit Tool: On-Page Checks, Crawl & Broken Links](https://apify.com/private_image_hg9/seo-audit-onpage)
- [Sitemap URL Extractor: All Site URLs, Lastmod & News](https://apify.com/private_image_hg9/sitemap-url-extractor)
- [Bulk URL Status Checker: Redirect Chain & Response Time](https://apify.com/private_image_hg9/bulk-url-status-redirect-checker)
- [Wayback Machine Scraper: Snapshots, Changes & Page Text](https://apify.com/private_image_hg9/wayback-machine-snapshots)
- [Website Screenshot & PDF: Full Page, Mobile, Batch URLs](https://apify.com/private_image_hg9/website-screenshot-pdf)

# Actor input Schema

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

One per line: a domain (acme.com), a host (shop.acme.co.uk), a web address (https://www.acme.com/about) or an email address (jane@acme.com). Registration is looked up for the registrable domain (acme.co.uk); DNS, email, SSL and HTTP checks use the host as given. Repeats are checked once.

## `checks` (type: `array`):

Which checks to run. All are on by default. Turning some off makes runs faster; the price per domain stays the same.

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

DKIM keys can only be found by name. These common selectors are always checked: google, selector1, selector2, k1, k2, s1, s2, default, dkim, mail. Add your own (for example from a DKIM-Signature header, the s= value) to check them too.

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

How many domains are checked in parallel. Each registry's RDAP server still gets at most 2 requests at a time.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "github.com",
    "bbc.co.uk"
  ],
  "checks": [
    "registration",
    "dns",
    "email",
    "ssl",
    "http"
  ],
  "maxConcurrency": 10
}
```

# Actor output Schema

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

One row per domain: registration, DNS, email authentication, SSL and HTTP, with the issues found.

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

Counts for the run: domains checked, not registered, charged, issues by severity, and whether the spending cap was reached.

# 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",
        "github.com",
        "bbc.co.uk"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("private_image_hg9/domain-whois-dns-ssl-checker").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",
        "github.com",
        "bbc.co.uk",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("private_image_hg9/domain-whois-dns-ssl-checker").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",
    "github.com",
    "bbc.co.uk"
  ]
}' |
apify call private_image_hg9/domain-whois-dns-ssl-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,private_image_hg9/domain-whois-dns-ssl-checker"
        }
    }
}
```

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/CpWhb26rHVcX6YmNN/builds/Efowj82wzCMSgDEFV/openapi.json
