# SiteVerdict — Domain & Email Health Checker (`siteverdict/siteverdict-domain-email-health`) Actor

Check SPF, DKIM, DMARC, BIMI, MTA-STS, TLS-RPT, DNSSEC, CAA, SSL certificate expiry, HSTS and security headers for one or thousands of domains. Get a 0-100 score, deliverability risk, plain-English explanations, exact DNS fixes and an HTML report.

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

## Pricing

$10.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

## SiteVerdict — Domain & Email Health Checker

**Check SPF, DKIM, DMARC, BIMI, MTA-STS, TLS-RPT, DNSSEC, CAA, SSL certificate expiry, HSTS and HTTP security headers for one domain or thousands.** Every domain gets a **0–100 score and A+ to F grade**, a **deliverability risk rating**, **plain-English explanations** and the **exact DNS record to paste** to fix each problem. You also get a shareable **HTML report** for every domain.

No browser, no login, no API keys. It only reads public DNS, HTTPS and TLS data, so a domain usually takes **about 1 second**.

### Who it's for

- **Agencies and MSPs**: audit every client domain in one run and send each client their own HTML report.
- **Email marketers and cold-outreach teams**: confirm you meet the **Gmail, Yahoo and Microsoft bulk-sender rules** (SPF + DKIM + DMARC) before you send.
- **Sales teams**: qualify leads by email security posture (provider, DMARC policy, broken SPF) at scale.
- **IT and security teams**: watch certificate expiry, HSTS, DNSSEC and header hygiene across a domain portfolio. Schedule it daily and alert on changes.

### What it checks

| Area | Checks |
|---|---|
| **MX** | Records and priorities, hosts that don't resolve, MX pointing at an IP, null MX (RFC 7505), **email provider detection** (Google Workspace, Microsoft 365, Zoho, Proton, Fastmail, Mimecast, Proofpoint, MessageLabs, IONOS, GoDaddy and 25+ more) |
| **SPF** | Syntax, exactly one record, **full recursive include/redirect expansion with the RFC 7208 10-lookup limit**, void lookups (limit 2), `+all` / `?all` / `~all` / `-all`, loops, deprecated `ptr`, oversized ranges, record length. **Sending services detected** from includes (SendGrid, Mailchimp, HubSpot, Salesforce, Amazon SES and more) |
| **DMARC** | Presence, `p=` / `sp=` / `np=`, `pct`, `rua` / `ruf` syntax (catches the missing `mailto:`), `adkim` / `aspf` alignment, duplicates, records at the wrong host, **external report authorisation records**, subdomains inheriting the organisational policy |
| **DKIM** | Probes about 45 common selectors (google, selector1/selector2, k1–k3, s1/s2, default, mandrill, smtpapi, zoho, mxvault, cf2024-1, pm, hs1/hs2, protonmail, fm1–fm3 and more), plus any you add. Reports **key type and length** (flags 1024-bit and weaker), revoked keys, `t=y` testing mode and wildcard answers |
| **BIMI** | Record, HTTPS logo URL, SVG Tiny PS profile, VMC/CMC certificate, and whether DMARC is strict enough for the logo to appear |
| **MTA-STS & TLS-RPT** | DNS record, policy file fetched over HTTPS (mode, max_age, **MX coverage**, redirects, content type), TLS-RPT `rua` |
| **SSL/TLS** (apex and www) | Chain validity (including incomplete chains), **days until expiry**, hostname match, issuer, key size, negotiated protocol, **TLS 1.0/1.1 still enabled** |
| **HTTPS & HSTS** | HTTP→HTTPS redirect chain (301 vs 302, hop count), HSTS max-age, includeSubDomains, **preload eligibility** (and optionally live status from hstspreload.org) |
| **Security headers** | Content-Security-Policy (including `unsafe-inline` / `unsafe-eval` analysis), X-Frame-Options / frame-ancestors, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, version leaks in Server / X-Powered-By |
| **DNS** | DNSSEC (DS at parent plus AD-flag validation; **detects broken "bogus" DNSSEC**), CAA (and whether your **current CA is allowed to renew**), nameserver count |
| **Optional, off by default** | DNS blocklists for MX IPs, and a polite SMTP STARTTLS capability probe (EHLO then QUIT only) |

Each check returns **pass / warn / fail / info / skipped**, a severity (critical, high, medium, low, info), a one-line summary, the issues found, and for each issue a **plain-English fix** and, where it applies, a **ready-to-paste record** such as:

```
TXT  _dmarc
v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; fo=1
```

### Scoring

- Each check has a weight. DMARC 20, SPF 15, SSL (apex) 12, DKIM 10, security headers 8, blocklists 6, MX / HTTPS redirect / HSTS 5, MTA-STS / DNSSEC / SSL (www) 4, STARTTLS 3, TLS-RPT / BIMI / CAA / nameservers 2.
- A pass earns the full weight and a warning earns part of it. Checks that don't apply are **left out of the score**: a domain that doesn't receive mail isn't penalised for missing MTA-STS, and disabled checks don't count.
- Any **critical** finding (expired certificate, `+all`, duplicate DMARC, broken DNSSEC) caps the score at 60. Any **high** finding caps it at 85.
- Grades: A+ ≥95, A ≥88, B ≥78, C ≥65, D ≥50, F <50. You also get separate `emailScore` and `webScore` values.
- **Deliverability risk** (low / medium / high / critical) is based only on email authentication, MX health and blocklists.
- **`bulkSenderReady.gmailYahoo`** is true when SPF, DKIM and DMARC meet the DNS prerequisites of the Gmail/Yahoo (Feb 2024) and Outlook.com (May 2025) bulk-sender requirements.

### Input

```json
{
  "domains": ["example.com", "https://www.yourclient.co.uk/", "info@another.org"],
  "maxConcurrency": 10,
  "extraDkimSelectors": ["mycustomselector"],
  "checkBlocklists": false,
  "smtpStarttlsCheck": false
}
```

- **domains**: bare domains, URLs or email addresses. They are normalised (IDN → punycode, `www.` stripped) and de-duplicated.
- **domainsListUrl**: for bulk runs, a URL to a TXT/CSV file with one domain per line.
- **skipChecks**: leave out any check, for example only email checks or only web checks.
- **dnsResolver**: Cloudflare (default), Google, Quad9, system, or DNS-over-HTTPS only. Failed lookups automatically fall back to DoH.
- **maxConcurrency** (default 10, max 50) and **delayBetweenDomainsMs** control speed and politeness for large lists.

### Output

One dataset item per domain. The dataset has four views: **Overview**, **Email deliverability**, **Website & TLS**, and **Fix list** (one row per prioritised fix). Export as JSON, CSV, Excel or HTML, or use the API. The key-value store also holds:

- `REPORT`: combined HTML report with a sortable summary table and full detail for the lowest-scoring domains
- `report-<domain>`: a standalone HTML report for each domain, ready to send to a client (`reportUrl` in each item)
- `SUMMARY`: run totals, grade and risk distribution

#### Sample (bbc.co.uk, trimmed)

```json
{
  "domain": "bbc.co.uk",
  "score": 88,
  "grade": "A",
  "emailScore": 85,
  "webScore": 98,
  "deliverabilityRisk": { "level": "medium", "reasons": ["No DKIM key found on common selectors (verify your real selector)."] },
  "emailProvider": "Broadcom Email Security.cloud (MessageLabs)",
  "spfStatus": "pass",
  "spfLookups": 4,
  "dmarcPolicy": "reject",
  "dkimSelectorsFound": [],
  "mtaStsMode": null,
  "dnssec": false,
  "sslDaysLeft": 104,
  "sslIssuer": "GlobalSign nv-sa",
  "hsts": true,
  "topFixes": [
    { "check": "MTA-STS", "severity": "low",
      "fix": "No MTA-STS. Mail sent to you can be downgraded to unencrypted SMTP ... Host a policy file at https://mta-sts.bbc.co.uk/.well-known/mta-sts.txt ...",
      "record": { "type": "TXT", "host": "_mta-sts", "value": "v=STSv1; id=2026101101" } }
  ],
  "checks": { "spf": { "status": "pass", "summary": "SPF found: 4/10 lookups, ends with soft fail (~all).", "...": "..." } }
}
```

Sample run on 11 Oct 2026: gov.uk **A (90)**, bbc.co.uk **A (88)**, github.com **B (85)**, google.com **B (81)**, example.com **C (75)**. Five domains took about 2 s in total.

### Pricing

Pay per event: you are charged **once per domain that is checked successfully** (`domain-checked`). Invalid inputs, domains that don't exist (NXDOMAIN) and domains that time out are **not charged**. See the Pricing tab for the current price. Set a maximum cost per run and the Actor stops cleanly when it is reached.

### FAQ

**Can it find every DKIM key?**
No tool can. DNS has no way to list DKIM selectors, so this Actor probes about 45 common ones. If a domain signs with a custom selector, look at the `s=` value in the `DKIM-Signature` header of an email it sent and add it under **Extra DKIM selectors**. The report says clearly when DKIM is "not found on common selectors" rather than claiming it's missing.

**Does it send email or log in to mail servers?**
No. It never sends email and never tries to authenticate or test for open relays. The optional STARTTLS probe (off by default) only connects, sends `EHLO`, reads the capability list and sends `QUIT`.

**Why are blocklist checks off by default?**
Blocklist operators set their own usage terms. Spamhaus, URIBL, SURBL and Barracuda restrict commercial or high-volume use, require a registered key, and often refuse queries from cloud or public resolvers. The default lists (SpamCop, PSBL, DroneBL) allow free low-volume lookups. You can add other zones only if your use complies with their terms. Answers that mean "query refused" (for example 127.255.255.x) are reported as unknown, not as listed.

**Is it gentle enough for thousands of domains?**
Yes. Results are cached across domains (for example `_spf.google.com` is resolved once per run), in-flight DNS queries are capped, and each domain makes only about 6 HTTP/TLS connections. Concurrency and per-worker delay are configurable. If the run migrates or restarts, it resumes without re-charging domains it already checked.

**Why does my site lose points for TLS 1.0/1.1 or missing headers when Google also lacks them?**
The score reflects best practice, not popularity. Every finding has a severity, so you can decide what matters to you, or skip whole checks with `skipChecks`.

**Can I schedule it?**
Yes. Use Apify Schedules (for example daily) and webhooks or integrations to alert when `sslDaysLeft` drops below 14 or a grade changes.

### Limitations

- DKIM is verified only for the selectors probed (see above).
- Blocklist checks look at **MX** IPs. Your outbound sending IPs, especially through an ESP, may be different.
- SPF and DMARC **alignment** in real mail can only be confirmed from message headers or DMARC reports. This Actor checks the published policy.
- DNSSEC status comes from a validating DoH resolver (Cloudflare, then Google).
- Sites with an invalid certificate are still analysed for TLS, but their HTTP headers can't be fetched. That case is reported as skipped.
- Port 25 is often blocked on cloud networks, so the STARTTLS probe may return "inconclusive".
- Results are a snapshot at check time. DNS changes take time to propagate.

### Data sources, compliance and retention

- **What it reads:** public DNS records (MX, SPF/TXT, DMARC, DKIM selectors, BIMI, MTA-STS/TLS-RPT, CAA, NS, DNSSEC) and the public HTTPS/TLS responses of the domains you enter. DNS queries go to the resolver you choose (default: Cloudflare 1.1.1.1; Google, Quad9 or your container's own resolver are options), with Cloudflare/Google DNS-over-HTTPS as a fallback.
- **Optional, off by default:** the hstspreload.org status API, DNS blocklists (Spamcop, PSBL and DroneBL by default, or zones you list) and an SMTP STARTTLS probe (EHLO then QUIT, no mail is sent). When switched on, those services receive the domain or mail-server names being checked.
- **No personal data** is collected on purpose; results describe domain configuration. DMARC report addresses are shown only as published in the domain's own DNS.
- **Where results are stored:** the default dataset and key-value store of the run (rows, `REPORT`, per-domain HTML reports, `SUMMARY`, and a `STATE` progress record), inside your own Apify account. These default storages are unnamed. How long Apify keeps them is set by Apify and your plan, not by SiteVerdict ([Apify data retention](https://docs.apify.com/platform/storage/usage#data-retention)); you can delete them sooner in Apify Console.
- **What SiteVerdict keeps:** nothing. The Actor runs in your Apify account and sends no data to SiteVerdict.
- **Not a certificate:** this is an automated configuration check, not a security audit, penetration test or legal advice.

### Changelog

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

***

Built by SiteVerdict. Feedback and feature requests are welcome in the Issues tab.

# Changelog

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

# Actor input Schema

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

Domains to check, one per line. You can paste bare domains (example.com), URLs (https://www.example.com/page) or email addresses (info@example.com); they are normalised and de-duplicated.

## `domainsListUrl` (type: `string`):

Optional URL of a plain-text or CSV file with one domain per line (first column is used). Handy for agencies checking thousands of client domains.

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

Safety cap on the number of domains processed in one run.

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

How many domains are checked in parallel. 10 is fast and polite; raise to 25-50 for large lists.

## `delayBetweenDomainsMs` (type: `integer`):

Optional pause after each domain, per worker. Use for extra-gentle rate limiting.

## `skipChecks` (type: `array`):

Checks to leave out (skipped checks are excluded from the score).

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

DKIM selectors cannot be listed via DNS, so common ones are probed. Leave empty to use the built-in list (google, selector1, selector2, k1, s1, s2, default, mandrill, smtpapi, zoho, mxvault, cf2024-1 and ~30 more).

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

Additional selectors to probe on top of the list above (e.g. the s= value from a DKIM-Signature header in one of your emails).

## `checkWww` (type: `boolean`):

Run the SSL/TLS check on www.<domain> as well as the apex.

## `stripWww` (type: `boolean`):

Treat www.example.com as example.com (email records live on the apex).

## `checkHstsPreloadList` (type: `boolean`):

Also ask the public hstspreload.org API whether the domain is on the Chrome HSTS preload list. Off by default to keep runs fully self-contained.

## `checkBlocklists` (type: `boolean`):

Look up MX server IPs on DNS blocklists. OFF by default. Blocklist operators set their own usage terms: the default lists (SpamCop, PSBL, DroneBL) allow free low-volume queries. Spamhaus, URIBL, SURBL and Barracuda restrict commercial use or require a registered key; only add them if your use complies with their terms.

## `dnsblZones` (type: `array`):

DNSBL zones to query when blocklist checks are enabled. Zones containing 'dbl' / 'uribl' / 'surbl' are queried with the domain name; others with MX IPs.

## `smtpStarttlsCheck` (type: `boolean`):

Connect to the primary MX on port 25, send EHLO, read the capability list and QUIT. No email is sent, no authentication or relay test is attempted. Off by default; port 25 is often blocked on cloud networks.

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

Recursive resolver used for lookups. Falls back to DNS-over-HTTPS (Cloudflare, then Google) on errors.

## `dohFallback` (type: `boolean`):

Retry failed lookups over DNS-over-HTTPS.

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

Timeout for each DNS/TLS/HTTP operation.

## `domainTimeoutSecs` (type: `integer`):

Hard limit for all checks of one domain.

## `htmlReport` (type: `boolean`):

Save a combined HTML report (key-value store record REPORT).

## `perDomainHtmlReports` (type: `boolean`):

Also save one shareable HTML report per domain (record report-<domain>); its URL is in each dataset item.

## `maxDetailedInSummary` (type: `integer`):

How many of the lowest-scoring domains get full detail in the combined report (all domains appear in the summary table).

## Actor input object example

```json
{
  "domains": [
    "gov.uk",
    "github.com",
    "example.com"
  ],
  "maxDomains": 10000,
  "maxConcurrency": 10,
  "delayBetweenDomainsMs": 0,
  "skipChecks": [],
  "checkWww": true,
  "stripWww": true,
  "checkHstsPreloadList": false,
  "checkBlocklists": false,
  "dnsblZones": [
    "bl.spamcop.net",
    "psbl.surriel.com",
    "dnsbl.dronebl.org"
  ],
  "smtpStarttlsCheck": false,
  "dnsResolver": "cloudflare",
  "dohFallback": true,
  "timeoutSecs": 6,
  "domainTimeoutSecs": 45,
  "htmlReport": true,
  "perDomainHtmlReports": true,
  "maxDetailedInSummary": 100
}
```

# Actor output Schema

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

No description

## `report` (type: `string`):

No description

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

No description

## `files` (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": [
        "gov.uk",
        "github.com",
        "example.com"
    ],
    "dnsblZones": [
        "bl.spamcop.net",
        "psbl.surriel.com",
        "dnsbl.dronebl.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("siteverdict/siteverdict-domain-email-health").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": [
        "gov.uk",
        "github.com",
        "example.com",
    ],
    "dnsblZones": [
        "bl.spamcop.net",
        "psbl.surriel.com",
        "dnsbl.dronebl.org",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("siteverdict/siteverdict-domain-email-health").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": [
    "gov.uk",
    "github.com",
    "example.com"
  ],
  "dnsblZones": [
    "bl.spamcop.net",
    "psbl.surriel.com",
    "dnsbl.dronebl.org"
  ]
}' |
apify call siteverdict/siteverdict-domain-email-health --silent --output-dataset

```

## MCP server setup

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

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/QjqZC40hwhrIloJJ0/builds/4XH4ZXIVpa6UdFmXD/openapi.json
