# Domain Health Checker: DNS, SPF, DKIM, DMARC, SSL & Headers (`data-done-right/domain-health-checker`) Actor

Check domain health in bulk: DNS records, SPF (with lookup count), DMARC policy, DKIM on common selectors, TLS certificate expiry and security headers. One flat row, issue list and 0-100 health score per domain. Public DNS plus one HTTPS GET. Pay per domain checked.

- **URL**: https://apify.com/data-done-right/domain-health-checker.md
- **Developed by:** [Data Done Right](https://apify.com/data-done-right) (community)
- **Categories:** Developer tools, SEO tools, Agents
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.24 / 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.
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 Health Checker: DNS, SPF, DKIM, DMARC, SSL & Headers

**Domain Health Checker** checks a list of domains in bulk and returns one flat row per domain: DNS records, SPF (with the DNS lookup count), DMARC policy, DKIM keys under common selectors, TLS certificate expiry and security headers, plus an issue list and a 0 to 100 health score.

> **What this does NOT do.** It does not send any email and does not talk to mail servers. DKIM is checked on about 20 common selector names only, so "not found" never means "no DKIM". DMARC is read at `_dmarc.<your domain>` only; there is no fallback to a parent domain. Security headers come from one plain HTTPS GET of the home page (no browser, no login, no bot-protection workarounds); sites that refuse the request are reported as `blocked` and not judged. It is not a vulnerability scanner, a blocklist or reputation check, or a WHOIS tool, and it does not scan ports.

**What you get**

- One row per domain with `healthScore`, `issues[]` (code, severity, message) and the raw values behind them.
- Email authentication: `spfStatus`, `spfPolicy`, `spfLookups`, `dmarcPolicy`, `dmarcPct`, report addresses, `dkimSelectorsFound`, MX hosts.
- Web security: `tlsStatus`, issuer, expiry date and days left, HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy.

Typical use: audit client or portfolio domains, find certificates that are about to expire, check SPF/DMARC before a campaign, or add a quick domain check to an AI agent or automation. Works through the Apify API, CSV/Excel/JSON export and as an MCP tool.

### How to check SPF, DKIM and DMARC for many domains

1. Paste the domains into **Domains**, one per line (URLs and emails are reduced to the host name).
2. Leave **Check TLS certificate** and **Check security headers** off if you only want email authentication; this makes the run faster.
3. Start the run and read `spfStatus`, `dmarcStatus`, `dmarcPolicy` and `dkimSelectorsFound` in the **Email authentication** view.
4. Filter on `issues` for the problems (for example `spf-missing`, `dmarc-policy-none`, `spf-too-many-lookups`).

If your mail service uses its own DKIM selector, add it under **Extra DKIM selectors** (the selector is the part before `._domainkey` in the DNS name).

### How to check SSL certificate expiry for a list of domains

Turn **Check DKIM** and **Check security headers** off, paste your domains and run. `tlsStatus`, `tlsExpires` and `tlsDaysLeft` are in the **TLS and headers** view. Certificates that expire within 30 days raise a `tls-expires-soon` issue (high at 14 days or less). Expired, self-signed and wrong-host certificates are reported with their own status. The check connects to port 443 of the domain itself; subdomains such as `shop.example.com` are checked as given.

### How to audit security headers for a website

Turn **Check DKIM** off, keep **Check security headers** on and run. The Actor sends one HTTPS GET to `https://<domain>/`, follows up to 5 redirects, and reports the six headers listed above for the final response. A missing header becomes an issue (HSTS and CSP medium, the others low). A CSP with `frame-ancestors` counts as clickjacking protection, so a missing X-Frame-Options is then not flagged.

### Input

```json
{
  "domains": ["python.org", "github.com", "example.com"],
  "checkDkim": true,
  "checkTls": true,
  "checkHeaders": true,
  "maxDomains": 100
}
```

| Field | Default | What it does |
|---|---|---|
| `domains` | 3 sample domains | Domains to check, one per line. URLs and email addresses are reduced to the host name. Required. |
| `maxDomains` | 100 | Maximum domains per run (1 to 10,000); caps the cost. |
| `checkDkim` | true | Look up DKIM keys under about 20 common selectors. |
| `extraDkimSelectors` | empty | Your own selector names, tried in addition to the common ones. |
| `checkTls` | true | Read the TLS certificate on port 443. |
| `checkHeaders` | true | One HTTPS GET for the security headers. |
| `dnsResolver` | `public` | `public` (Cloudflare, Google, Quad9), `cloudflare`, `google`, `quad9` or `system`. |
| `concurrency` | 5 | Domains checked in parallel (1 to 20). |
| `timeoutSecs` | 15 | Timeout for the TLS handshake and the HTTPS request (3 to 60). |

### Output

Real output of a local run for `github.com` (shortened):

```json
{
  "input": "github.com",
  "domain": "github.com",
  "status": "ok",
  "healthScore": 97,
  "issueCount": 1,
  "issues": [{"code": "missing-permissions-policy", "severity": "low", "message": "Response has no permissions-policy header"}],
  "mxRecords": ["0 github-com.mail.protection.outlook.com"],
  "hasMx": true,
  "spfStatus": "valid",
  "spfPolicy": "softfail",
  "spfLookups": 10,
  "dmarcStatus": "valid",
  "dmarcPolicy": "quarantine",
  "dkimStatus": "found",
  "dkimSelectorsFound": ["google", "selector1", "k1", "k2", "k3", "s1", "s2", "zendesk1", "zendesk2", "cm"],
  "dkimSelectorsChecked": 22,
  "tlsStatus": "valid",
  "tlsIssuer": "Sectigo Limited",
  "tlsExpires": "2027-01-27",
  "tlsDaysLeft": 84,
  "headersStatus": "ok",
  "hsts": "max-age=31536000; includeSubdomains; preload",
  "missingSecurityHeaders": ["permissions-policy"],
  "dnsResolver": "public",
  "checkedAt": "2026-11-04T14:53:04+00:00"
}
```

Other fields: `ipv4`, `ipv6`, `nameservers`, `caaRecords`, `txtRecordCount`, `nullMx`, `spfRecord`, `dmarcRecord`, `dmarcSubdomainPolicy`, `dmarcPct`, `dmarcReportAddresses`, `tlsSubject`, `tlsProtocol`, `tlsError`, `httpsStatusCode`, `httpsFinalUrl`, `httpsRedirects`, `csp`, `xContentTypeOptions`, `xFrameOptions`, `referrerPolicy`, `permissionsPolicy`, `headersError`. The `SUMMARY` record has counts by status, the average health score and the DKIM selectors tried. Dates and certificate values depend on the day you run it.

Row `status`: `ok`, `nxdomain` (the domain does not exist), `invalid` (not a domain name), `dns-error` (DNS failed temporarily, try again), `failed` (unexpected error). Problem inputs are also listed in the `FAILED` record.

### How the health score works

`healthScore` = 100 minus 20 for each high issue, 8 for each medium and 3 for each low (never below 0). It is a transparent checklist, not a security rating, and it is not comparable with other tools' scores. Every deduction is visible in `issues`.

| Area | Issue codes (severity) |
|---|---|
| SPF | `spf-missing` (high if the domain has MX, else low), `spf-multiple-records` (high), `spf-pass-all` (high), `spf-neutral` and `spf-none` (medium), `spf-too-many-lookups` (high, more than 10) |
| DMARC | `dmarc-missing` (high if the domain has MX, else low), `dmarc-invalid` and `dmarc-multiple-records` (high), `dmarc-policy-none` (medium), `dmarc-pct-below-100` and `dmarc-no-reporting` (low) |
| DKIM | `dkim-not-found` (low, only for domains with MX) |
| DNS | `caa-missing` (low) |
| TLS | `tls-expired`, `tls-hostname-mismatch`, `tls-self-signed`, `tls-untrusted`, `tls-not-yet-valid` (high), `tls-expires-soon` (high at 14 days or less, medium at 30), `https-unreachable` (medium) |
| Headers | `missing-strict-transport-security` and `missing-content-security-policy` (medium), the other four (low) |

Notes on accuracy:

- `spfLookups` counts include, a, mx, ptr, exists and redirect terms (RFC 7208) and follows include/redirect (at most 12 fetches, `spfLookups` can then be a lower bound). It does not expand the hosts behind an `mx` term.
- `tlsStatus: untrusted` can also mean a certificate higher up the chain failed validation on the machine running the Actor; the error text says so and gives the site certificate's own expiry date.
- Domains without A/AAAA records get `no-web-host` and skip the TLS and header checks.

### How much does it cost

One event, **`domain-checked`: $0.0028 per domain with a DNS verdict** ($2.80 per 1,000). Not charged: invalid inputs, `dns-error` rows (temporary DNS failures), `failed` rows and duplicates. A domain that does not exist (`nxdomain`) is charged, because that is a definite answer.

| Domains | Cost |
|---|---|
| 1,000 | $2.80 |
| 10,000 | $28.00 |
| 100,000 | $280.00 |

Set `maxDomains` or a spending limit on the run to cap the bill; the Actor stops cleanly when the limit is reached.

### Use cases

- Audit the email authentication of client domains before a mail migration or campaign.
- Monitor certificate expiry across a portfolio of domains (run it on a schedule).
- Find domains with no DMARC, a `p=none` policy or a broken SPF record.
- Compare security headers across your own sites or after a hosting change.
- Give an AI agent a one-call "is this domain set up properly" check.

### FAQ

**Does it prove a domain has no DKIM?** No. DKIM keys can only be found by selector name and the selector is the sender's choice. About 20 common ones are tried (google, selector1, selector2, k1, default, s1, mandrill and others); add your own under Extra DKIM selectors. `dkimStatus: not-found-in-common-selectors` is a hint, and it is only a low-severity issue.

**Which DNS servers are used?** Public resolvers (Cloudflare, Google, Quad9 in turn), or the platform default if you choose `system`. Very large lists can hit resolver rate limits; failures show as `dns-error`, are not charged, and can be re-run.

**Does it check the parent domain for DMARC?** No. For `mail.example.com` it reads `_dmarc.mail.example.com` only, even though receivers may fall back to the organisational domain. Check the registered domain too.

**Can it check internal or private hosts?** No. Hosts that resolve to private, loopback or link-local addresses are refused, for both the TLS and the HTTP check (`blocked`).

**Why does a site show `headersStatus: blocked`?** The site answered 401, 403, 429 or 451 to a plain request. The Actor does not work around bot protection, so it does not judge those headers.

**Does it test cipher suites, TLS versions or vulnerabilities?** No. It reports the negotiated TLS version and the certificate only.

**Does it follow redirects from http to https?** No. It requests `https://<domain>/` only and does not test the plain-HTTP redirect.

### Use with the Apify API, MCP, n8n and Make

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("YOUR_USERNAME/domain-health-checker").call(run_input={"domains": ["example.com", "python.org"]})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["domain"], row["healthScore"], [i["code"] for i in row["issues"]])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('YOUR_USERNAME/domain-health-checker').call({ domains: ['example.com', 'python.org'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((r) => [r.domain, r.healthScore, r.tlsDaysLeft]));
```

In n8n or Make use the Apify node/module "Run Actor", pass `domains` and read the dataset items. For AI agents the Actor is available through the Apify MCP server; the input is one `domains` array.

### Related tools

- Verify Email Addresses in Bulk: Syntax, MX & Disposable (check the mailboxes behind the domains)
- HTTP Status Code Checker: Bulk URL, Website & Redirect Chains
- Domain Age Checker: Registration Date, Expiry & RDAP Lookup
- Website Technology Checker: CMS, WordPress & Tracking Pixels

# Actor input Schema

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

Domains to check, one per line (use Bulk edit to paste a long list). Example: example.com. A URL such as https://www.example.com/page is reduced to www.example.com; subdomains are checked as given. Duplicates are removed.

## `maxDomains` (type: `integer`):

Upper limit of domains processed in this run. It also caps the cost: at $0.0028 per domain, 100 domains cost at most $0.28. Example: 100.

## `checkDkim` (type: `boolean`):

Look up DKIM keys under about 20 common selectors (google, selector1, selector2, k1, default, s1 ...). DKIM keys can only be found by selector name, so a key under a custom selector is not found. Adds about 20 DNS queries per domain.

## `extraDkimSelectors` (type: `array`):

Your own DKIM selector names to try in addition to the common ones, one per line. Example: mta2024. Only used when Check DKIM is on.

## `checkTls` (type: `boolean`):

Open one TLS connection to port 443 and read the certificate: validity, issuer, expiry date and days left. Expired, self-signed and wrong-host certificates are reported.

## `checkHeaders` (type: `boolean`):

Send one HTTPS GET to the home page of the domain (redirects followed) and report HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy. Only the response headers are read.

## `dnsResolver` (type: `string`):

Which DNS servers answer the lookups. The default tries Cloudflare, Google and Quad9 in turn. Switch only if you see many dns-error rows.

## `concurrency` (type: `integer`):

Domains checked in parallel. Example: 5. Higher values are faster but can trigger DNS rate limits.

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

Time allowed for the TLS handshake and for the HTTPS request per domain. Example: 15.

## Actor input object example

```json
{
  "domains": [
    "python.org",
    "github.com",
    "example.com"
  ],
  "maxDomains": 100,
  "checkDkim": true,
  "extraDkimSelectors": [],
  "checkTls": true,
  "checkHeaders": true,
  "dnsResolver": "public",
  "concurrency": 5,
  "timeoutSecs": 15
}
```

# Actor output Schema

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

One row per domain: health score, issues, DNS, SPF, DMARC, DKIM, TLS certificate and security header results (all items as JSON, CSV, Excel and more).

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

SUMMARY record: counts by status, average health score, DKIM selectors tried, charge count and limits.

## `records` (type: `string`):

Key-value store records of this run, including FAILED and NOTES when present.

# 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": [
        "python.org",
        "github.com",
        "example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("data-done-right/domain-health-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": [
        "python.org",
        "github.com",
        "example.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("data-done-right/domain-health-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": [
    "python.org",
    "github.com",
    "example.com"
  ]
}' |
apify call data-done-right/domain-health-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,data-done-right/domain-health-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/rbd3ZH5YWGPPtl5ox/builds/ReRUL6qcabKCBf1jm/openapi.json
