# SPF, DMARC, DKIM & SSL Checker – Bulk Domain Health Audit (`shephouse/domain-health-audit-bulk`) Actor

Bulk email security audit for many domains: SPF checker with the 10-lookup limit, DMARC checker, DKIM probe, MTA-STS, SSL certificate expiry checker, security-header grade and tech stack. Issue codes per domain. $0.009 per domain audit + $0.00005 per run.

- **URL**: https://apify.com/shephouse/domain-health-audit-bulk.md
- **Developed by:** [Jacob Calmelat](https://apify.com/shephouse) (community)
- **Categories:** Developer tools, SEO tools, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $9.00 / 1,000 domain audits

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

## SPF, DMARC, DKIM & SSL Checker – Bulk Domain Health Audit

Audit a list of domains in one run and get a structured health report per domain: **DNS, email authentication (SPF incl. the 10-lookup limit, DMARC, DKIM, MTA-STS), TLS certificate expiry, HTTP security-header grade, homepage SEO basics and the technologies a site uses** – plus a plain list of issue codes you can filter on.

It only uses **public DNS, a TLS handshake and a few polite web requests per domain** (robots.txt, one homepage request, one HTTP→HTTPS redirect check). No logins, no scraping of page content, no personal data.

**SPF checker, DMARC checker, DKIM check and SSL certificate expiry checker in one bulk run.** An email security audit for MSPs, agencies and anyone managing many domains.

#### What a full run gives you

One row per domain with stable issue codes you can filter on. Example (fixture showing the shape; see the real `example.com` result under *Output* below):

```json
{"domain": "client-a.example", "status": "OK",
 "email": {"spf": {"policy": "SOFTFAIL", "dnsLookups": 12}, "dmarc": {"policy": "none"}},
 "tls": {"daysUntilExpiry": 9, "issuer": "..."},
 "issues": ["SPF_TOO_MANY_LOOKUPS", "DMARC_POLICY_NONE", "DMARC_NO_REPORTING", "TLS_CERT_EXPIRES_LT_14D"]}
```

- **Price:** $0.009 per audited domain + $0.00005 per run. Invalid, duplicate and non-existent domains are not charged. Set "Max cost per run" to cap spend.
- **Free Apify plan:** the first 25 domains of each run are audited so you can check the output on your own domains; the rest are skipped and not charged. A paid Apify plan audits the whole list (up to 10,000 domains per run) at the same price per domain.

### What you get for each domain

| Section | Fields |
|---|---|
| **DNS** | resolves, IPv4 sample, IPv6 count, nameservers, CAA present |
| **Email** | MX hosts, **mail provider** (Google Workspace, Microsoft 365, Proofpoint, Mimecast …), **SPF** (policy, total DNS lookups across includes, `+all`/`ptr`/multiple-record problems), **DMARC** (policy, `pct`, reporting configured – addresses are never output), DKIM selectors found (common selectors, extendable), MTA-STS, null-MX |
| **TLS** | protocol version, issuer, expiry date, **days until expiry**, validity (expired, self-signed, hostname mismatch, untrusted issuer), SAN count |
| **Website** | final URL, status code, response time, redirect chain, HTTP→HTTPS redirect, `robots.txt` / sitemap declared, **security-header grade A–F** (HSTS, CSP, X-Content-Type-Options, clickjacking protection, Referrer-Policy, Permissions-Policy), title / meta description / canonical / H1 count / viewport / Open Graph / hreflang / JSON-LD types |
| **Technologies** | CMS, e-commerce, JS frameworks, analytics & ad pixels, marketing automation, chat, CDN/hosting, web server, payment, consent tools (≈70 signatures) |
| **Issues** | stable codes such as `SPF_MISSING`, `DMARC_POLICY_NONE`, `SPF_TOO_MANY_LOOKUPS`, `TLS_CERT_EXPIRES_LT_14D`, `NO_HSTS`, `MISSING_TITLE` |

### Typical uses

- **Agencies & MSPs** – monthly health report for all client domains; alert on certificates expiring in < 30 days.
- **Email deliverability** – find domains with no DMARC, `p=none`, SPF over the 10-lookup limit (a silent cause of failed mail) before Google/Yahoo/Microsoft bulk-sender rules bite.
- **Sales & lead qualification** – enrich a prospect list with mail provider, CMS/e-commerce platform and marketing stack; find prospects with obvious security or SEO gaps.
- **Security & compliance hygiene** – baseline security headers, TLS posture and email authentication across a portfolio of domains.
- **Due diligence / M\&A** – quick technical snapshot of many acquired or target domains.

### How it compares

Checked on the Apify Store on 2026-10-23 (prices change; verify before you decide):

| | **This Actor** | Single-purpose SSL or DNS checkers | Broader "security audit" Actors |
|---|---|---|---|
| Typical price per domain | **$0.009** (+ $0.00005 per run) | about $0.001 to $0.01 | about $0.02 to $0.03 |
| TLS expiry and validity | yes | usually the only thing checked | yes |
| SPF incl. 10-lookup count, DMARC, DKIM probe, MTA-STS | yes | rarely | varies |
| Security-header grade, SEO basics, detected technologies | yes | no | varies |
| Stable issue codes to filter on | yes | rarely | varies |
| Pay only for domains actually audited | yes: invalid, duplicate and non-existent domains are free | varies | varies |

**Choose something else if** you only need a certificate-expiry list for a handful of domains (a single-purpose checker can be cheaper), or if you need vulnerability scanning, port scanning or authenticated testing, which this Actor deliberately does not do.

### Automate it: monthly reports, alerts and AI agents

- **Schedule** the Actor (Apify Schedules) and add a webhook or integration (for example Make, Zapier, n8n, Google Sheets or Slack through Apify's integrations) to be notified of rows whose `issues` contain `TLS_CERT_EXPIRES_LT_30D`, `DMARC_MISSING` or `SPF_TOO_MANY_LOOKUPS`.
- **API**: start a run and read the dataset with the Apify API or the official clients (Python, JavaScript). Set **Maximum cost per run** to cap spend: the Actor stops exactly at your limit.
- **AI agents**: the Actor can be called from MCP-compatible assistants through the Apify MCP server. The input is a plain list of domains and the output is flat, documented JSON with issue codes.

### Recipes: audit client domains from code, n8n or a schedule

**curl (synchronous, waits up to 300 s; good for a few hundred domains):**

```bash
curl -X POST "https://api.apify.com/v2/actors/shephouse~domain-health-audit-bulk/run-sync-get-dataset-items?maxTotalChargeUsd=2" \
  -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
  -d '{"domains":["your-client.com"],"checkEmail":true,"checkTls":true,"checkWebsite":false}'
```

`maxTotalChargeUsd` is a hard cap on what the call can charge. For bigger lists start a run with the API or an official client and read the dataset when it finishes.

**Keep only the rows that need action** (n8n *Code* node; the same logic works in any language):

```js
const bad = new Set(['SPF_MISSING','SPF_ALLOWS_ALL','SPF_TOO_MANY_LOOKUPS','DMARC_MISSING','DMARC_POLICY_NONE',
  'TLS_CERT_EXPIRED','TLS_CERT_EXPIRES_LT_14D','TLS_CERT_EXPIRES_LT_30D','TLS_CERT_INVALID']);
return $input.all().filter(i => (i.json.issues || []).some(c => bad.has(c)));
```

**Monthly audit for an MSP:** save your domain list as a task, add an Apify Schedule (for example `0 7 1 * *`) and a "run succeeded" webhook that calls n8n, Make, Zapier or Slack. 200 domains cost $1.80 per run; 500 cost $4.50. The free Apify plan audits at most 25 domains per run. Audit only domains you manage or have permission to assess.

### Input

```json
{
  "domains": ["example.com", "https://www.iana.org/domains", "your-client.com"],
  "maxItems": 100,
  "checkEmail": true,
  "checkTls": true,
  "checkWebsite": true,
  "dkimSelectors": ["myselector"]
}
```

- `domains` – domains or URLs, one per line. Only the host is audited; paths are ignored. IP addresses, `localhost` and internal names are rejected.
- `maxItems` – hard cap per run (default 100, max 10,000).
- Switch off sections you do not need (`checkEmail`, `checkTls`, `checkWebsite`).
- `dkimSelectors` – DKIM selector names cannot be discovered from DNS, so common ones (`google`, `selector1`, `selector2`, `k1`, `s1`, `default` …) are probed. Add your own to extend.

### Output (one dataset item per domain; real result for `example.com`, shortened)

```json
{
  "input": "example.com",
  "domain": "example.com",
  "status": "OK",
  "billable": true,
  "detail": null,
  "auditedAt": "2026-10-23T18:25:32+00:00",
  "dns": {
    "resolves": true,
    "ipv4": [
      "172.66.147.243",
      "104.20.23.154"
    ],
    "ipv6Count": 2,
    "nameservers": [
      "elliott.ns.cloudflare.com",
      "hera.ns.cloudflare.com"
    ],
    "hasCaa": false
  },
  "email": {
    "hasMx": false,
    "mxCount": 0,
    "mxHosts": [],
    "mailProvider": null,
    "nullMx": true,
    "spf": {
      "present": true,
      "multiple": false,
      "record": "v=spf1 -all",
      "all": "-all",
      "policy": "FAIL",
      "includes": [],
      "redirect": null,
      "lookupTerms": 0,
      "hasPtr": false,
      "dnsLookups": 0,
      "lookupCountTruncated": false,
      "includeLoop": false
    },
    "dmarc": {
      "present": true,
      "multiple": false,
      "policy": "reject",
      "subdomainPolicy": "reject",
      "pct": 100,
      "aggregateReportAddresses": 0,
      "forensicReportAddresses": 0,
      "adkim": "s",
      "aspf": "s"
    },
    "dkimSelectorsFound": [],
    "dkimNote": "best-effort: only common/provided selector names are probed",
    "mtaSts": false
  },
  "tls": {
    "checked": true,
    "protocol": "TLSv1.3",
    "verified": true,
    "notAfter": "2026-12-25T22:56:35+00:00",
    "notBefore": "2026-09-26T22:49:11+00:00",
    "daysUntilExpiry": 63,
    "issuer": "SSL Corporation",
    "subjectCommonName": "example.com",
    "sanCount": 2
  },
  "website": {
    "reachable": true,
    "finalUrl": "https://example.com/",
    "statusCode": 200,
    "responseTimeMs": 27,
    "redirectChain": [],
    "contentType": "text/html",
    "server": "cloudflare",
    "httpsUsed": true,
    "securityHeaders": {
      "score": 0,
      "maxScore": 8,
      "grade": "F",
      "present": {
        "strict-transport-security": false,
        "content-security-policy": false,
        "x-content-type-options": false,
        "x-frame-options": false,
        "referrer-policy": false,
        "permissions-policy": false
      }
    },
    "page": {
      "title": "Example Domain",
      "titleLength": 14,
      "metaDescription": null,
      "metaDescriptionLength": 0,
      "canonical": null,
      "lang": "en",
      "hasViewport": true,
      "h1Count": 0,
      "hreflangCount": 0,
      "openGraph": false,
      "twitterCard": false,
      "robotsMeta": null,
      "generator": null,
      "structuredDataTypes": []
    },
    "httpToHttpsRedirect": false
  },
  "technologies": [
    {
      "name": "Cloudflare",
      "category": "CDN"
    }
  ],
  "issues": [
    "NO_HTTPS_REDIRECT",
    "NO_HSTS",
    "NO_CSP",
    "MISSING_META_DESCRIPTION",
    "MISSING_H1",
    "NO_CANONICAL",
    "NO_OPEN_GRAPH"
  ]
}
```

Runs also save a `SUMMARY` record (delivered, billable, skipped, stop reason).

### Pricing – pay per audited domain

- **$0.009 per domain audited** (= $9 per 1,000) – event `domain-audited`.
- **$0.00005 per run** (Apify's standard start event; charged once per GB of run memory, so once at the default 512 MB).
- No other charges. You are **never charged** for: invalid input, duplicates, domains that do not exist, DNS temporary failures, domains pointing to private/reserved addresses, audits that failed internally, or status rows.
- A domain counts as audited when it has DNS records (a website is **not** required – email-only domains are audited too).
- Your **Maximum cost per run** is respected exactly: the Actor stops delivering when the next domain would exceed it (the start fee counts against that cap). Example: a cap of $0.03 delivers 3 domains; a cap below $0.00905 delivers none and returns one free `MAX_COST_TOO_LOW` row.
- Free Apify plan runs are limited to **25 domains per run**.

### How it behaves (and what it does not do)

- **Polite by design**: per domain at most `robots.txt`, one homepage request (HTTPS, with an HTTP fallback only if HTTPS is unavailable; following ≤5 redirects) and one HTTP→HTTPS redirect check that does not download a page. The homepage is **not requested if `robots.txt` disallows it** for automated agents (rows then show `robotsDisallowed`; DNS/TLS findings are still delivered). Requests identify themselves as `ShepHouseAudit`.
- **No personal data collected**: only technical metadata. Email addresses in DMARC records or on pages are never output; only counts.
- **SSRF-safe**: targets resolving to private, loopback, link-local or reserved addresses are never contacted – including redirect targets.
- **Not a vulnerability scanner**: no probing of ports, paths or services. Header and TLS grading is a hygiene indicator, not a penetration-test result.
- **Technology detection is signature-based** (headers, cookies, script/link URLs, meta tags). It can miss tools loaded dynamically or behind bot protection, and can occasionally be fooled by references to third-party assets.
- Product and company names mentioned (for example Google Workspace, Microsoft 365, Cloudflare) are trademarks of their owners and appear only to describe what was detected. No affiliation is implied.
- **Sites that block bots** (HTTP 403/429, challenges) return partial results; the status code is reported.
- **DKIM discovery is best-effort** (selector names are not enumerable). Wildcard DNS answers are detected and ignored.
- SPF lookup counting follows `include`/`redirect` up to 25 DNS queries and flags when truncated.
- Results are a point-in-time snapshot from the Actor's network location.

### FAQ

**Is this legal / allowed?** It reads public DNS records, performs a standard TLS handshake and requests a public homepage once, honouring `robots.txt` – what a browser or search engine does. You are responsible for auditing only domains you are entitled to assess and for complying with applicable law.

**How do I find domains whose SSL/TLS certificate expires soon?** Run the Actor on your domain list and filter the dataset for `tls.daysUntilExpiry` below your threshold, or for the issue codes `TLS_CERT_EXPIRES_LT_14D` / `TLS_CERT_EXPIRES_LT_30D`. Schedule it weekly to be warned in time.

**How do I check SPF, DMARC and the SPF 10-lookup limit for many domains at once?** Paste the domains. Each row reports the SPF policy and total DNS lookups across includes (flagged as `SPF_TOO_MANY_LOOKUPS` above 10), the DMARC policy and the issue codes `SPF_MISSING`, `DMARC_MISSING`, `DMARC_POLICY_NONE`.

**Is a "not audited" or free row a bug?** No. Rows with `billable: false` explain why (invalid input, domain does not exist, temporary DNS failure, blocked non-public address). You are never charged for them.

**Do you check that mailboxes exist?** No. This Actor does not contact mail servers.

**Why was I not charged for a row?** Check `status` and `billable`: only `OK` rows are billable.

**Can I schedule it?** Yes – use Apify Schedules and a webhook/integration to alert on `issues` such as `TLS_CERT_EXPIRES_LT_14D`.

### Changelog

- **0.1.6** – listing and documentation: clearer title and description (SPF, DMARC, DKIM, SSL), a "what a full run gives you" section, clearer input help and free-plan message. No change to audit logic, billing or pricing.
- **0.1.5** – documentation only: recipes for curl, n8n filtering and a monthly MSP schedule, with cost examples. No change to behavior or pricing.
- **0.1.4** – documentation only: comparison table, automation and AI-agent section, more FAQ answers. No change to behavior or pricing.
- **0.1.3** – the pre-filled example input is now one domain (`example.com`), so a run with the minimum cost limit ($0.01) audits it completely. The output example is now a real `example.com` result. No pricing or behavior change.
- **0.1** – first release: DNS, email authentication (SPF with recursive lookup count, DMARC, DKIM probe, MTA-STS), TLS, homepage checks with RFC 9309 robots.txt handling, ≈70 technology signatures, issue codes, pay-per-domain pricing, per-run caps, SSRF guard.

### Support

Open an issue on this Actor's page with the domain, the run ID and what you expected. Please do not post customer data publicly.

# Actor input Schema

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

One domain or URL per line, e.g. example.com or https://www.example.com/page (only the host is audited). Audit only domains you manage or may assess. Duplicates, invalid and non-existent domains are not charged. Free Apify plan: first 25 domains per run.

## `maxItems` (type: `integer`):

Stop after this many domains (max 10,000 per run). Together with 'Max cost per run' in Run options it caps your spend: $0.009 per audited domain + $0.00005 per run.

## `checkEmail` (type: `boolean`):

SPF checker (policy, +all, multiple records, DNS lookups vs the 10-lookup limit), DMARC checker (policy, pct, reporting), DKIM probe, MTA-STS, MX and mail provider. DNS only.

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

SSL/TLS certificate expiry date and days left, issuer, validity (expired, self-signed, hostname mismatch) and protocol version. Flags certificates expiring in under 14 or 30 days.

## `checkWebsite` (type: `boolean`):

Security-header grade A-F (HSTS, CSP ...), redirects, SEO basics and detected technologies. One polite request for robots.txt and the homepage per domain; skipped if robots.txt disallows it. Turn off for a DNS + TLS-only audit.

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

Optional. DKIM selector names cannot be discovered from DNS, so common ones (google, selector1, selector2, k1, s1, default ...) are probed. Add your own, e.g. the selector your mail provider gave you.

## Actor input object example

```json
{
  "domains": [
    "example.com"
  ],
  "maxItems": 100,
  "checkEmail": true,
  "checkTls": true,
  "checkWebsite": true,
  "dkimSelectors": []
}
```

# Actor output Schema

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

No description

## `summary` (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": [
        "example.com"
    ]
};

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

# Run the Actor and wait for it to finish
run = client.actor("shephouse/domain-health-audit-bulk").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": [
    "example.com"
  ]
}' |
apify call shephouse/domain-health-audit-bulk --silent --output-dataset

```

## MCP server setup

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

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/7OxQNaDCFJpQM0Zjy/builds/0SQT3iH5G8Uinfeyr/openapi.json
