# Domain Health Audit: DNS, WHOIS, SSL, SPF, DKIM & DMARC (`apifmcpfactory/domain-health-audit`) Actor

Audit 1 to 5,000 domains for DNS, WHOIS, SSL, HTTP and email-authentication problems, and get the exact DNS records to publish to fix each one. Scored, exportable, with a client-ready report. Validated against RFC 9989. For people and AI agents.

- **URL**: https://apify.com/apifmcpfactory/domain-health-audit.md
- **Developed by:** [Howth Technology Factory](https://apify.com/apifmcpfactory) (community)
- **Categories:** Developer tools, Agents, SEO tools
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 domain auditeds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Domain Health Audit: DNS, WHOIS, SSL, SPF, DKIM & DMARC

Audit 1 to 5,000 domains for DNS, WHOIS, SSL, HTTP and email-authentication problems — and get the exact DNS records to publish to fix every one of them.

Paste your domains, press Start. Every other field has a working default.

***

### What it does

Point it at a list of domains and it returns a scored, exportable audit of everything published about them: DNS records, registration and expiry, the website certificate, HTTP and security headers, and the whole email-authentication stack — SPF, DKIM, DMARC, MX, MTA-STS, TLS-RPT, DNSSEC, DANE, reverse DNS and BIMI.

Each domain gets a 0–100 score, a letter grade, a pass/fail verdict against the Gmail and Outlook bulk-sender requirements, and a ranked list of what to fix first. Every problem comes with a copy-paste DNS record, not a link to a specification.

Then it produces the report — a client-ready HTML and PDF document with your own logo on the cover.

### Who it's for

- **Managed service providers** auditing a client's estate before onboarding, or proving value at a QBR.
- **Digital agencies** who inherit domains and need to know what they have taken on.
- **In-house IT and security** managing a portfolio — not one domain, but forty, or four hundred.
- **AI agents** that need structured, machine-readable domain posture with an explicit confidence signal on every result.

If you manage one domain, a free single-domain checker will do. This exists for portfolios.

### What you get

- **A score you can defend.** The full rubric is published below and returned inline with every run. You can show a client exactly why a domain scored 64.
- **The fix, not just the finding.** Every problem carries the record to publish: type, name, TTL and value, correct for the actual defect.
- **SPF flattening that shows its work.** When a record breaks the 10-lookup limit, you get a rewritten record that fits, a diff of which includes were expanded, and an explicit warning about what you are taking on by hard-coding a supplier's addresses.
- **A staged DMARC rollout plan** — monitor, then quarantine in test mode, then quarantine, then reject — with the exact record at each stage.
- **A sender inventory that names names.** Every SPF include and mail server resolved to the actual service: Google Workspace, Microsoft 365, Marketo, Proofpoint, Mimecast, and 30 more. An include a client cannot account for is a conversation worth having.
- **Validated against the current DMARC standard.** RFC 9989 was published on 19 May 2026 and removed the `pct`, `rf` and `ri` settings, and replaced the Public Suffix List with a DNS tree walk. Records written against the previous version are flagged as non-conformant.
- **Change tracking across runs.** Schedule it and get told what changed — new problems, resolved problems, records that were edited, and score movement — with a webhook when something regresses.
- **Honest confidence.** Where a result cannot be proven, it says so rather than guessing. See the limits section.

### Positioning

The enterprise DMARC platforms start around $5,000/yr for 25 domains and charge separately for reporting, white-labelling and monitoring. The free single-domain checkers hand you raw records and leave you to interpret them. Running `dig` by hand across 400 domains is a day's work that has to be repeated every quarter.

This sits in between: portfolio-scale, one price, everything included — sender inventory, subdomain discovery, white-label reporting, monitoring and change alerts are all in the per-domain price, not gated behind a tier.

### Use cases

| Situation | How to run it |
|---|---|
| Onboarding a new client | `full` preset, generate the report, put your logo on it |
| Quarterly review across the estate | `standard` preset, schedule monthly, `onlyReportChanges` on |
| "Are we ready for the Gmail requirements?" | `mandate` preset — just the mandate checks and the verdict |
| Triaging a 2,000-domain acquisition | `quick` preset first to sort, then `standard` on what matters |
| Catching an expiring certificate or domain | Schedule weekly with a webhook and an alert threshold |
| Feeding an AI agent | Any preset; read the dataset, every field is structured |

### How it works

```
  your domains
       |
       v
  [ input hygiene ]   strip scheme/path/port/www, punycode, de-duplicate,
       |              report malformed entries instead of dropping them
       v
  [ resolution ]      each domain's own nameservers, asked directly
       |              (negatives re-checked against a recursive resolver)
       v
  [ checks ]  DNS · MX · SPF · DKIM · DMARC · MTA-STS · TLS-RPT
       |      DNSSEC · DANE · reverse DNS · WHOIS/RDAP · SSL · HTTP · BIMI
       v
  [ scoring ]         HTF-Score v1.0 — published rubric, unassessable
       |              components excluded rather than failed
       v
  [ fix-it pack ]     exact records, SPF flattening, staged DMARC rollout
       |
       v
  dataset  +  report (HTML / PDF)  +  change tracking
```

### Sample output

One row per domain in the default view. These are real values from a run on 2026-07-31 — the expiry counters obviously move:

| domain | grade | score | googleReady | microsoftReady | criticalCount | topFix | certExpiresInDays | domainExpiresInDays |
|---|---|---|---|---|---|---|---|---|
| rte.ie | A+ | 95 | true | true | 0 | Remove the obsolete pct setting | 174 | 2800 |
| github.com | C | 67 | true | true | 0 | Remove the obsolete pct setting | 61 | 70 |
| example.com | C | 67 | not-applicable | not-applicable | 1 | Renew the domain registration | 88 | 12 |

Two things in that table are worth pointing at. Both `rte.ie` and `github.com` publish a `pct=` setting, which RFC 9989 removed on 19 May 2026 — a live example of the check on well-configured, well-known domains. And `example.com` returns `not-applicable` rather than `false`, because it publishes a null MX record stating it handles no mail; reporting it as failing the mandates would invent work that does not exist.

Each finding in the full view looks like this:

```json
{
  "code": "SPF_LOOKUP_LIMIT_EXCEEDED",
  "title": "Your SPF record needs too many DNS lookups",
  "severity": "high",
  "whyItMatters": "Mail servers give up after 10 lookups. Yours needs more, so some providers will fail your SPF (Sender Policy Framework) check and your mail may be rejected.",
  "evidence": { "lookupCount": 14, "limit": 10, "chain": ["1. include:_spf.example.net", "..."] },
  "fix": {
    "recordType": "TXT", "host": "@", "ttl": 3600,
    "value": "v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.0/24 -all",
    "note": "This replaces 9 lookups with 12 address ranges, bringing the record to 0 lookups. Flattening hard-codes your suppliers' current IP ranges..."
  },
  "scoreImpact": -4,
  "acknowledged": false
}
```

### What this does NOT do

Stated plainly, because a tool that overstates its coverage is worse than one that admits its edges.

- **DKIM cannot be proven absent.** Signing keys are published under a selector name known only to you and your email provider. The naming is not listed anywhere in public records and cannot be enumerated, so when we report that no key was found we say how many names we tested. That is never the same as saying DKIM is not configured. Supply your selector names and the result becomes definitive — and until you do, the DKIM score is `unknown`, not zero, and the mandate verdicts return `unknown` rather than `false`.
- **No blocklist or reputation checks.** The major blocklist operators prohibit commercial use of their free query services. Rather than breach those terms or pass the cost on, we leave the check out entirely and say so. If you need blocklist status, query it directly under your own licence.
- **No live SMTP conversation.** This audit does not open mail connections to your servers. That means no banner check, no test of which encryption options a server advertises, no inspection of the certificate a mail server actually presents, and no end-to-end verification of DANE. Everything reported about mail transport comes from published records rather than a live session.
- **BIMI is a draft, not a finished standard.** Brand Indicators for Message Identification is still a draft specification and is not endorsed as an internet standard. Support varies between mailbox providers. We check the record, validate the logo file format and read the issuer and expiry from the certificate; confirming that certificate chains to an approved root requires a separate trust store we do not bundle.
- **Sending infrastructure is not visible from outside.** Reverse DNS is checked on the mail servers published to *receive* your mail, because those are the ones listed in public records. The servers you send from are frequently different and are not published anywhere, so they cannot be assessed here.
- **Subdomain discovery is not an exhaustive inventory.** Public records do not permit listing every name under a domain. Discovery finds names referenced by other records, commonly used, or visible in public certificate logs when that option is enabled.
- **This is a technical audit, not legal or compliance advice.** Findings describe published configuration and what changing it would do. They are not a certification of compliance with any provider policy, regulation or contract.

**Scope of the bulk-sender verdicts.** This audit measures what is published in DNS and on your public web and mail hosts. The bulk-sender requirements from the major mailbox providers also cover things that cannot be assessed from the outside: reverse DNS on the servers you actually *send* from, encryption in transit on outbound connections, keeping spam complaints below 0.3%, and one-click unsubscribe in marketing messages. A passing result here means your published configuration is correct; it is not a certification that you meet every bulk-sender requirement.

### How the score is calculated — HTF-Score v1.0

Published in full so any figure can be checked and reproduced, and returned inline with every run.

| Component | Points | How it is awarded |
|---|---|---|
| DMARC | 25 | `reject` 25, `quarantine` 18, `none` 8, absent 0. Deduct 4 if no `rua`, 3 if `pct=` is present (removed by RFC 9989), 3 if an external report destination is unauthorised |
| SPF | 15 | Present 6, valid syntax 3, within the 10-lookup limit 4, ends `-all` or `~all` 2. **Zero outright if more than one SPF record is published** |
| DKIM | 10 | At least one valid selector 7, key 1024 bits or longer 3. **Scored `unknown` rather than 0** when no selector was supplied and none could be inferred |
| MX | 10 | Present and resolving 7, no invalid configuration 3. A deliberate null MX scores full marks |
| MTA-STS | 7 | Record plus a fetchable, valid policy 4, `enforce` mode 3 |
| SSL certificate | 8 | Valid and hostname-matching 5, more than 30 days to expiry 3 |
| DNSSEC | 4 | Signed and validating |
| TLS-RPT | 3 | Present and valid |
| PTR / FCrDNS | 3 | All published mail hosts forward-confirmed. Inbound mail-host hygiene — not a measure of bulk-sender compliance |
| Domain expiry | 4 | More than 60 days to expiry. Excluded where the registry publishes no expiry date |
| BIMI | 5 | Valid record 3, verified logo certificate present and unexpired 2 |
| HTTPS + HSTS | 5 | HTTP redirects to HTTPS 2, HSTS `max-age` of at least one year 3 |
| DANE | 1 | TLSA records present on the mail hosts. Presence and syntax only |
| **Total** | **100** | |

**Grades:** A+ 95–100 · A 85–94 · B 75–84 · C 60–74 · D 40–59 · F below 40

**A component that cannot be assessed is excluded from the calculation, never scored zero.** A `.de` domain whose registry publishes no expiry date is not penalised for it; the component leaves the denominator and the per-domain results list what was excluded and why.

### Questions

**How is the score calculated?** The whole rubric is in the table above and is returned with every run. Nothing about it is hidden.

**Why does it say "unknown" instead of yes or no?** Because we could not prove it. DKIM signing keys cannot be discovered from public records, so if you have not supplied your selector names we say what we tested rather than guessing. Supply them and the answer becomes definitive.

**What happens to a domain that does not exist?** It returns a plain-English error, is excluded from the score and the report, and **is not charged for**.

**Can I use my own branding?** Yes. Set your company name and logo and the report cover becomes entirely yours — our name appears only as a single attribution line at the back. There is no extra charge for white-labelling.

**How does change tracking work across runs?** Results are stored per domain in a named key-value store, so a scheduled run compares against the last time that domain was seen, not just against other domains in the same batch. Turn on `onlyReportChanges` to return only what moved.

**What does acknowledging a finding do?** It stops alerting on it and drops it out of the priority list. It stays in the results marked as acknowledged, and **it still counts against the score** — accepting a problem does not make it go away.

**Is this fast enough for thousands of domains?** Yes. Lookups are cached within a run, domains are audited in parallel, and each domain has its own time limit — one slow domain returns a partial result instead of stalling the run.

### Trust and limits

Charges apply only to domains successfully audited. Malformed entries, domains that do not exist and domains that could not be checked at all are never billed. Batch size is bounded at 5,000 domains per run. Nothing is stored beyond your own run storage and the per-domain history used for change tracking. This is a technical audit, not legal or compliance advice.

### Using this from an AI agent

The dataset is fully structured — every finding carries a machine-readable `code`, a human-readable `title` and `whyItMatters`, structured `evidence`, and a `fix` object with the record to publish.

Fields worth reading programmatically:

- `score` (0–100) and `grade` (A+ … F)
- `googleReady` / `microsoftReady` / `bimiReady` — `true`, `false`, `"unknown"` (cannot be proven) or `"not-applicable"` (the domain handles no mail). **Never treat `"unknown"` as `false`.**
- `findings[]` — each with `code`, `severity`, `whyItMatters`, `evidence`, `fix`, `scoreImpact`, `acknowledged`
- `scoring.components` — per-component earned/possible with a `status` of `scored`, `unknown` or `unavailable`
- `scoring.excludedComponents` — what was left out of the calculation, and why
- `mandateScopeNote` — what the verdicts do and do not cover
- `topFix` — a plain-English next action, safe to surface directly to a user

This is also available as an MCP server, so an agent can call it as a tool rather than reading a dataset.

# Actor input Schema

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

One domain per line. Paste plain domains (example.com), full web addresses (https://example.com/page) or email addresses (someone@example.com) — anything extra is stripped for you. Duplicates are removed. Up to 5,000 domains per run.

## `auditPreset` (type: `string`):

Choose the depth of the audit. Standard is right for almost everyone. Pick Custom only if you want to turn individual checks on and off yourself.

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

Only applies when 'How much to check' is set to Custom.

## `checkMx` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

## `checkSpf` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

## `checkDmarc` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

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

Only applies when 'How much to check' is set to Custom.

## `checkWhois` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

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

Only applies when 'How much to check' is set to Custom.

## `checkHttp` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

## `checkMtaSts` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

## `checkTlsRpt` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

## `checkDnssec` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

## `checkDane` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

## `checkFcrdns` (type: `boolean`):

Only applies when 'How much to check' is set to Custom.

## `checkBimi` (type: `boolean`):

Only applies when 'How much to check' is set to Custom. BIMI is a draft specification, not a finished standard.

## `checkSubdomains` (type: `boolean`):

Only applies when 'How much to check' is set to Custom. Finds subdomains that send or receive mail and checks whether they are protected.

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

The names your email provider publishes your signing keys under — for example 'google', 'selector1', 'k1'. Supplying them is the ONLY way to get a definitive DKIM answer: selector names cannot be discovered from public records, so without them a negative result means 'not found at the names we tested', never 'not configured'. Your provider's setup page lists them.

## `deepDkimScan` (type: `boolean`):

Tries a large list of selector names used across the industry. Slower and charged per domain. Only worth it when you do not know your selectors and the standard list found nothing.

## `generateReport` (type: `boolean`):

Builds a client-ready report covering the whole run: overall grade, what is failing the Gmail and Outlook requirements, a ranked list of fixes, and a page per domain with the exact records to publish.

## `reportFormat` (type: `string`):

HTML opens in any browser and prints cleanly to A4. PDF is ready to send to a client. Both produces the two.

## `reportCompanyName` (type: `string`):

Appears on the cover. Setting this (or a logo) puts the report in white-label mode: the cover becomes entirely yours.

## `reportLogoUrl` (type: `string`):

A direct link to a PNG, JPG or SVG. Shown on the cover in place of ours.

## `reportAccentColor` (type: `string`):

Hex colour used for headings and highlights, for example #4338CA.

## `reportFooterText` (type: `string`):

Appears at the bottom of every page — usually your company name or a client reference.

## `onlyReportChanges` (type: `boolean`):

For scheduled runs. Compares against the previous result for each domain and returns only those whose score, findings or records have changed. The first run always returns everything.

## `alertThreshold` (type: `string`):

How serious a new problem must be before the webhook fires.

## `webhookUrl` (type: `string`):

Called with a JSON summary when a domain gets worse — a lower score or a new problem at or above your threshold. Leave empty for no alerts.

## `acknowledgedFindings` (type: `array`):

Entries in the form example.com:SPF\_LOOKUP\_LIMIT\_EXCEEDED. These stop triggering alerts and drop out of the priority list, but they still appear in the results marked as acknowledged, and they still count against the score — accepting a problem does not make it go away.

## `timeoutPerDomainSecs` (type: `integer`):

A domain that runs out of time returns whatever finished, marked as a partial result, rather than failing the run.

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

Higher is faster on large lists. Lower if you are auditing infrastructure that rate-limits you.

## `cacheTtlSecs` (type: `integer`):

Avoids repeating identical lookups within a run, which matters when a list shares parent domains.

## `useAuthoritativeResolvers` (type: `boolean`):

Reads records from the source rather than a shared cache, so you see what is published right now. Turn off only if your network blocks direct nameserver queries.

## `enableCtEnrichment` (type: `boolean`):

Finds additional subdomains that have ever been issued a certificate. Off by default: it relies on a third-party service with no commercial terms, so it is never required and is skipped silently if unavailable.

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "github.com",
    "nasa.gov"
  ],
  "auditPreset": "standard",
  "checkDns": true,
  "checkMx": true,
  "checkSpf": true,
  "checkDmarc": true,
  "checkDkim": true,
  "checkWhois": true,
  "checkSsl": true,
  "checkHttp": true,
  "checkMtaSts": true,
  "checkTlsRpt": true,
  "checkDnssec": true,
  "checkDane": true,
  "checkFcrdns": true,
  "checkBimi": true,
  "checkSubdomains": false,
  "deepDkimScan": false,
  "generateReport": false,
  "reportFormat": "both",
  "reportAccentColor": "#4338CA",
  "onlyReportChanges": false,
  "alertThreshold": "high",
  "timeoutPerDomainSecs": 60,
  "maxConcurrency": 10,
  "cacheTtlSecs": 300,
  "useAuthoritativeResolvers": true,
  "enableCtEnrichment": false
}
```

# Actor output Schema

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

One record per domain: score, grade, bulk-sender readiness, and every finding with the exact DNS record to publish.

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

The client-ready report. Only present when a report was requested.

## `reportPdf` (type: `string`):

The same report as a print-ready A4 PDF. Only present when a report was requested.

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

Portfolio rollup, ranked remediation queue, the published scoring rubric and the stated limits of what was measured.

# 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": [
        "google.com",
        "github.com",
        "nasa.gov"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("apifmcpfactory/domain-health-audit").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": [
        "google.com",
        "github.com",
        "nasa.gov",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("apifmcpfactory/domain-health-audit").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    "google.com",
    "github.com",
    "nasa.gov"
  ]
}' |
apify call apifmcpfactory/domain-health-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=apifmcpfactory/domain-health-audit",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/CezRHulUaDfaaYnEJ/builds/iL8plmBQ4CIqIVRqJ/openapi.json
