# SPF, DMARC & DKIM Checker — Bulk Domain WHOIS, DNS & SSL (`owlabee/spf-dmarc-dkim-checker`) Actor

Bulk SPF, DMARC, DKIM, MTA-STS, TLS-RPT, DANE and DNSSEC checks with an email security score, plus RDAP WHOIS, DNS, TLS certificate expiry, HTTP headers and tech stack. Opt-in: security.txt, AI-crawler rules, llms.txt, ASN and hosting. One JSON record per domain, pay per domain.

- **URL**: https://apify.com/owlabee/spf-dmarc-dkim-checker.md
- **Developed by:** [Owlabee](https://apify.com/owlabee) (community)
- **Categories:** Lead generation, 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

from $1.28 / 1,000 domain reports

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

## SPF, DMARC & DKIM Checker — Bulk Domain WHOIS, DNS & SSL

![SPF, DMARC & DKIM Checker — Bulk Domain WHOIS, DNS & SSL: banner with the main output fields](https://api.apify.com/v2/key-value-stores/r8pLvIribNPHYI9gX/records/domain-intelligence-banner.png)

**Bulk SPF, DMARC and DKIM checker** with WHOIS and DNS lookup: check hundreds or thousands of domains in one run and get one JSON record per domain with an email-security score (SPF, DMARC, DKIM, MTA-STS, BIMI), mail transport checks (TLS-RPT, DNSSEC validation, DANE, MTA-STS policy vs MX, CAA), RDAP WHOIS registration, DNS records, SSL certificate expiry, security headers and tech stack hints. Two opt-in sections add security.txt, AI-crawler rules and other well-known files, and the IP, ASN and hosting provider. The `domains` list has no fixed item cap; up to 20 domains are checked in parallel (`maxConcurrency`), and the run stops cleanly when its maximum total charge is reached.

**Example.** Input `"domains": ["https://www.example.com/pricing", "jane@example.com"]` is reduced to one domain, `example.com`, and its record includes (values from the Actor's RDAP test fixture):

```json
"whois": { "registrar": "RESERVED-Internet Assigned Numbers Authority", "registrationDate": "1995-08-14T04:00:00.000Z", "expirationDate": "2026-08-13T04:00:00.000Z",
           "nameservers": ["a.iana-servers.net", "b.iana-servers.net"], "dnssecSigned": true, "registrantCountry": "US" }
```

The full record, with DNS, email security, TLS and HTTP sections, is in [Output example](#output-example) below.

> **Renamed on 2026-11-07:** this Actor's address changed from `owlabee/domain-intelligence` to `owlabee/spf-dmarc-dkim-checker`. Saved tasks, schedules, webhooks and integrations keep working, because they point to the Actor's ID, which did not change. Update anything that calls it by name: API URLs (`owlabee~spf-dmarc-dkim-checker`), MCP tool lists (`owlabee/spf-dmarc-dkim-checker`) and bookmarks.

### What it does

Give this Actor a list of domains and it returns **one enriched JSON record per domain** with everything a sales, security or IT team usually has to collect from four different tools:

- **WHOIS via RDAP** — registrar, registration / expiration / last-changed dates, days until the domain expires, EPP status codes, nameservers, DNSSEC, and registrant organisation / country when the registry publishes them (redacted fields stay `null`; nothing is guessed). The IANA RDAP directory covers all generic TLDs but few country codes, so the Actor also asks the registry of **.io and .me** (Identity Digital) directly.
- **DNS records** — A, AAAA, MX (sorted by priority), NS, TXT, CAA and SOA over DNS-over-HTTPS, plus a mail-provider guess (Google, Microsoft, Zoho, Proton, Fastmail, Mimecast, Proofpoint, other).
- **Email security** — SPF record and syntax check (lookup count, `-all` / `~all` / `?all`), DMARC policy, subdomain policy, `pct`, aggregate/forensic reporting addresses, DKIM selectors found (18 common selectors probed, plus your own), MTA-STS record and policy mode, BIMI record, and an **email security score 0–100**.
- **Mail transport and DNSSEC** — TLS-RPT record and its reporting addresses, whether the resolver **validates the zone with DNSSEC** (not just "signed at the registry"), DNSKEY algorithms and DS digest types, **DANE TLSA** records on up to 3 MX hosts, the MTA-STS policy's `mx:` patterns with any MX host they do not cover, the CA domains allowed by CAA and whether CAA allows the CA of the current certificate (`caaAllowsIssuer`), plus a plain-language `mailTransportIssues` list.
- **TLS certificate** — subject CN, issuer, valid-from / valid-to, days until expiry, SAN count, protocol version and whether the chain actually verifies.
- **HTTP** — final URL, status code, full redirect chain, `server` / `x-powered-by`, security headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy), page title, meta generator and **tech stack hints** (Shopify, WordPress, Wix, Squarespace, Webflow, HubSpot, Cloudflare, Next.js, Nuxt, Drupal, Ghost, Framer, Vercel, Netlify, Google Tag Manager, Google Analytics, Intercom, Stripe).
- **Well-known files** (opt-in section `wellknown`) — **security.txt** (RFC 9116: contacts, expiry and whether it has expired, policy, signed or not, problems found), **AI-crawler rules in robots.txt** per bot (GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-User, Claude-SearchBot, anthropic-ai, CCBot, Google-Extended, Applebot-Extended, PerplexityBot, Bytespider, Meta-ExternalAgent, Amazonbot) and whether robots.txt blocks everything, **llms.txt** (present, title, size), **ads.txt** (records, ad systems, DIRECT and RESELLER counts), Global Privacy Control (`gpc.json`), the change-password URL, the OpenID Connect issuer, and iOS and Android app links.
- **IP and hosting** (opt-in section `ip`) — for up to 2 IPv4 and 1 IPv6 address: ASN, network name, announced prefix, country, regional registry, allocation date, reverse DNS and a **hosting / CDN guess** (Cloudflare, AWS, Google Cloud, Azure, Akamai, Fastly, DigitalOcean, Hetzner, OVHcloud, Vercel, Netlify, Shopify, Squarespace, Wix, GitHub and others). Data from Team Cymru's IP-to-ASN service over DNS, with RIPEstat as the fallback.

Every section is independent: if a registry is slow or a site is down, the other sections are still filled in and the problem is recorded in `errors`. It only uses public RDAP, public DNS resolvers and the domain's own public endpoints — no API keys, no proxies, no browser — which is why it costs **$0.0015 per domain** (plus $0.0005 per opt-in section when you add `wellknown` or `ip`). Results are available as JSON, CSV, Excel, via API, webhooks or the Apify MCP server, like any Apify dataset.

Typical uses: lead enrichment and segmentation by tech stack and mail provider, email deliverability audits (SPF/DMARC/DKIM gaps), certificate and domain expiry monitoring, M\&A / vendor due diligence, and competitor technology research.

To watch what those domains publish afterwards, [Website Change Monitor — Sitemap Monitor & Page Diff](https://apify.com/owlabee/website-change-monitor) reports new, removed and changed pages between runs.

### Why this Actor

| | This Actor | Typical scraper in this category | Do it yourself |
|---|---|---|---|
| Data sources covered | RDAP registries, DNS-over-HTTPS resolvers (Cloudflare with Google as fallback), the domain's own TLS endpoint and landing page | WHOIS and DNS, or availability only; email security and tech stack usually need a second Actor | Four clients (RDAP, DoH, TLS socket, HTTP) and a scorer |
| Modes | Six independent sections switched on or off per run (`whois`, `dns`, `tls`, `http`, plus the opt-in `wellknown` and `ip`); each fails alone and the rest are still returned | A fixed bundle, or one lookup type per Actor | Whatever you wire together, with your own fallbacks |
| Output fields | One record per domain with nested sections plus flattened columns: registrar, expiry, mail provider, DMARC policy, TLS days left, HTTP status, tech hints, email security score, one-line summary | Raw WHOIS text or record lists; scoring is rare | Your schema |
| Pricing unit | Per domain record, whichever default sections run, plus a small per-section charge only for the two opt-in sections; an invalid input costs the same as a record | Per domain or per lookup type, so a full picture costs several lookups | Your time plus compute; the sources are free |
| Proxies or browsers needed | None | Some use proxies to work around WHOIS port-43 rate limits | WHOIS over port 43 is rate-limited; most RDAP servers are not |
| Agent / MCP ready | Yes: pay-per-event, limited permissions, pin link, prompts and a call example below | Sometimes, with an example prompt | You write and host the tool definition |
| Limits | 20 second budget per domain; DKIM is discovered from 18 common selectors plus yours, not enumerated; registrable-domain reduction uses a curated suffix list | Varies | Timeouts, redirects and resolver fallbacks are yours to handle |

- One run replaces four tools for a lead list or a deliverability audit: registration, DNS, SPF/DMARC/DKIM/MTA-STS/BIMI with a 0–100 score, certificate and tech stack.
- Sections are independent: a slow registry or a dead web server never blanks the other sections, and `errors` names the source that failed.
- Nothing is guessed: redacted WHOIS fields stay `null`, and `whois.source` tells you which server answered.
- Flattened columns make the Console table, the CSV export and a spreadsheet readable without unpacking JSON.
- Every record is charged as it is saved under a hard 20 second budget per domain, so a list of dead domains costs the same as a list of live ones and finishes in bounded time.

### How to use

1. Paste your **Domains** (one per line). URLs such as `https://www.example.com/pricing` and email addresses such as `jane@example.com` are fine — the registrable domain is extracted and duplicates are merged.
2. Optionally untick **Sections** you do not need or add the opt-in **Well-known files** and **IP, ASN & hosting** sections, add **Extra DKIM selectors** your ESP uses, and set **Max concurrency**.
3. Click **Start**. Each domain is written to the dataset as soon as it is finished; download the table or connect it to your CRM, spreadsheet or agent via the API.

#### Quick start input

The same input the Store runs in its daily automated test, so it is cheap and always available; paste it into the JSON tab of the input form or send it as the body of an API or MCP call:

```json
{
  "domains": [
    "example.com",
    "apify.com",
    "github.com"
  ],
  "sections": [
    "whois",
    "dns",
    "tls",
    "http"
  ]
}
```

#### Ready-to-run inputs

Three inputs you can paste as they are; each validates against the input schema.

**DNS and email-security audit of a customer list** (SPF, DMARC, DKIM, MTA-STS, BIMI and the score; add your ESP's DKIM selectors)

```json
{
  "domains": ["example.com", "apify.com", "github.com"],
  "sections": ["dns"],
  "dkimSelectors": ["fm1", "krs", "s1"],
  "maxConcurrency": 10
}
```

**WHOIS-only check: registrar, registration and expiry dates**

```json
{
  "domains": ["example.com", "example.org", "example.net"],
  "sections": ["whois"],
  "maxConcurrency": 5
}
```

**Security posture and hosting** (opt-in sections: security.txt, AI-crawler rules, llms.txt, ads.txt, ASN and hosting provider, plus the mail transport checks of the DNS section)

```json
{
  "domains": ["example.com", "github.com", "cloudflare.com"],
  "sections": ["dns", "wellknown", "ip"]
}
```

**Certificate and web stack only, from URLs and email addresses** (no registry or DNS calls)

```json
{
  "domains": ["https://www.example.com/pricing", "jane@github.com"],
  "sections": ["tls", "http"]
}
```

### Input

| Field | Type | Required | Description |
|---|---|---|---|
| `domains` | string\[] | yes | Domains, URLs or email addresses to check. Deduplicated by registrable domain. |
| `sections` | string\[] | no | Any of `whois`, `dns`, `tls`, `http`, `wellknown`, `ip`. Default: `whois`, `dns`, `tls`, `http` (the two newer sections run only when you add them, at $0.0005 per section per domain on top of the record). |
| `dkimSelectors` | string\[] | no | Extra DKIM selectors to probe in addition to the built-in 18. |
| `maxConcurrency` | integer | no | Domains checked in parallel (1–20, default 5). |
| `webhookUrl` | string | no | POST target for the run summary (counts, charges, finish time). |

### Output example

![Sample output of SPF, DMARC & DKIM Checker — Bulk Domain WHOIS, DNS & SSL as a table: the dataset overview columns from the quick-start run](https://api.apify.com/v2/key-value-stores/r8pLvIribNPHYI9gX/records/domain-intelligence-output.png)

```json
{
  "domain": "github.com",
  "input": "https://github.com/apify",
  "fetchedAt": "2026-10-16T12:00:00.000Z",
  "sections": ["whois", "dns", "tls", "http"],
  "whois": {
    "status": "ok",
    "source": "rdap.markmonitor.com",
    "registrar": "MarkMonitor Inc.",
    "registrarIanaId": "292",
    "registrationDate": "2007-10-09T18:20:50.000Z",
    "expirationDate": "2026-10-09T18:20:50.000Z",
    "lastChangedDate": "2024-09-07T09:16:33.000Z",
    "daysUntilExpiry": 358,
    "registered": true,
    "domainStatus": ["client delete prohibited", "client transfer prohibited", "client update prohibited"],
    "nameservers": ["dns1.p08.nsone.net", "dns2.p08.nsone.net", "ns-1283.awsdns-32.org", "ns-1707.awsdns-21.co.uk"],
    "dnssecSigned": false,
    "registrantOrg": "GitHub, Inc.",
    "registrantCountry": "US"
  },
  "dns": {
    "status": "ok",
    "resolver": "cloudflare",
    "nxdomain": false,
    "a": ["140.82.121.4"],
    "aaaa": [],
    "mx": [{ "priority": 1, "exchange": "aspmx.l.google.com" }, { "priority": 5, "exchange": "alt1.aspmx.l.google.com" }],
    "ns": ["dns1.p08.nsone.net", "dns2.p08.nsone.net"],
    "txt": ["v=spf1 ip4:192.30.252.0/22 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com include:spf.protection.outlook.com include:mail.zendesk.com include:_spf.salesforce.com include:servers.mcsv.net ip4:166.78.69.169 ip4:166.78.69.170 ip4:166.78.71.131 ip4:167.89.101.2 ip4:167.89.101.192/28 ip4:192.254.112.60 ip4:192.254.112.98/31 ip4:192.254.113.10 ip4:192.254.113.101 ip4:192.254.114.176 ip4:62.253.227.114 ~all"],
    "caa": [{ "flags": 0, "tag": "issue", "value": "digicert.com" }],
    "soa": { "mname": "dns1.p08.nsone.net", "rname": "hostmaster.nsone.net", "serial": 1718097213, "refresh": 43200, "retry": 7200, "expire": 1209600, "minimum": 3600 },
    "hasMx": true,
    "mxProviderGuess": "google",
    "spfRecord": "v=spf1 ip4:192.30.252.0/22 include:_netblocks.google.com ... ~all",
    "spfValid": true,
    "spfAllQualifier": "~all",
    "spfLookupCount": 7,
    "spfIssues": [],
    "dmarcRecord": "v=DMARC1; p=reject; pct=100; rua=mailto:dmarc@github.com",
    "dmarcPolicy": "reject",
    "dmarcSubdomainPolicy": null,
    "dmarcPct": 100,
    "hasRua": true,
    "hasRuf": false,
    "dmarcIssues": [],
    "dkimSelectorsProbed": 18,
    "dkimSelectorsFound": ["google", "pm"],
    "mtaStsPresent": true,
    "mtaStsRecord": "v=STSv1; id=20230901",
    "mtaStsPolicyMode": "enforce",
    "bimiRecord": null
  },
  "tls": {
    "status": "ok",
    "protocol": "TLSv1.3",
    "authorized": true,
    "authorizationError": null,
    "subjectCN": "github.com",
    "issuerOrg": "Sectigo Limited",
    "issuerCN": "Sectigo ECC Domain Validation Secure Server CA",
    "validFrom": "2026-02-05T00:00:00.000Z",
    "validTo": "2027-02-05T23:59:59.000Z",
    "daysUntilExpiry": 112,
    "sanCount": 2,
    "fingerprint256": "A1:B2:..."
  },
  "http": {
    "status": "ok",
    "finalUrl": "https://github.com/",
    "statusCode": 200,
    "redirectChain": [],
    "server": "github.com",
    "poweredBy": null,
    "contentType": "text/html; charset=utf-8",
    "securityHeaders": {
      "hsts": "max-age=31536000; includeSubdomains; preload",
      "csp": "default-src 'none'; base-uri 'self'; ...",
      "xFrameOptions": "deny",
      "xContentTypeOptions": "nosniff",
      "referrerPolicy": "origin-when-cross-origin, strict-origin-when-cross-origin"
    },
    "pageTitle": "GitHub · Build and ship software on a single, collaborative platform",
    "metaGenerator": null,
    "techHints": ["Stripe"]
  },
  "registrar": "MarkMonitor Inc.",
  "expirationDate": "2026-10-09T18:20:50.000Z",
  "mxProviderGuess": "google",
  "dmarcPolicy": "reject",
  "tlsDaysUntilExpiry": 112,
  "httpStatusCode": 200,
  "techHints": ["Stripe"],
  "emailSecurityScore": 95,
  "summary": "github.com: registered via MarkMonitor Inc., expires in 358 days; MX google, SPF ok (~all), DMARC reject, DKIM google/pm, MTA-STS; email security 95/100; TLS valid, cert 112 days left, by Sectigo Limited; HTTP 200, server github.com, tech: Stripe",
  "errors": {}
}
```

Records from 2026-11 on also carry the mail transport fields in `dns`, a top-level `caaAllowsIssuer`, and, when you add the opt-in sections, `wellknown`, `ip`, `hostingProvider` and `hasSecurityTxt` (each is `null` when its section did not run). Values from a local run on 2026-11-07, shortened (some fields and list entries left out):

```json
{
  "domain": "cloudflare.com",
  "dns": {
    "tlsRptRecord": "v=TLSRPTv1; rua=mailto:rua@cloudflare.com;",
    "tlsRptRua": ["mailto:rua@cloudflare.com"],
    "dnssecValidated": true,
    "dnskeyAlgorithms": [13],
    "dsDigestTypes": [2],
    "daneMxHostsChecked": 3,
    "daneMxHostsWithTlsa": ["mxa-canary.global.inbound.cf-emailsecurity.net", "mxb-canary.global.inbound.cf-emailsecurity.net", "mxa.global.inbound.cf-emailsecurity.net"],
    "tlsaRecords": [{ "host": "mxa-canary.global.inbound.cf-emailsecurity.net", "usage": 3, "selector": 1, "matchingType": 1, "validated": true }],
    "caaIssuers": ["comodoca.com", "digicert.com", "letsencrypt.org", "pki.goog", "ssl.com"]
  },
  "wellknown": {
    "status": "ok",
    "baseUrl": "https://cloudflare.com",
    "securityTxt": { "url": "https://www.cloudflare.com/.well-known/security.txt", "contact": ["https://hackerone.com/cloudflare", "https://www.cloudflare.com/abuse/"], "expires": null, "expired": null, "policy": ["https://www.cloudflare.com/disclosure/"], "hiring": ["https://www.cloudflare.com/careers/jobs/"], "preferredLanguages": "en", "signed": false, "issues": ["no Expires field (required)"] },
    "gpcSupported": null,
    "oidcIssuer": null,
    "adsTxt": { "present": false, "records": 0, "adSystems": 0, "direct": 0, "reseller": 0, "truncated": false },
    "aiCrawlerPolicy": { "GPTBot": "allowed", "OAI-SearchBot": "not-mentioned", "ClaudeBot": "not-mentioned", "anthropic-ai": "allowed", "CCBot": "allowed", "Google-Extended": "allowed", "PerplexityBot": "allowed" },
    "aiCrawlersBlocked": [],
    "robotsBlocksAll": false,
    "llmsTxt": { "present": true, "title": "Cloudflare", "bytes": null },
    "files": [{ "path": "/robots.txt", "status": 200 }, { "path": "/.well-known/security.txt", "status": 200 }, { "path": "/.well-known/gpc.json", "status": 404 }, { "path": "/ads.txt", "status": 404 }, { "path": "/llms.txt", "status": 200 }]
  },
  "ip": {
    "status": "ok",
    "addresses": [
      { "ip": "104.16.132.229", "version": 4, "asn": 13335, "asName": "CLOUDFLARENET - Cloudflare, Inc., US", "prefix": "104.16.128.0/20", "country": "US", "rir": "arin", "allocated": "2014-03-28", "ptr": null, "hostingGuess": "Cloudflare", "source": "cymru" },
      { "ip": "2606:4700::6810:84e5", "version": 6, "asn": 13335, "asName": "CLOUDFLARENET - Cloudflare, Inc., US", "prefix": "2606:4700::/44", "country": "US", "rir": "arin", "allocated": "2011-11-01", "ptr": null, "hostingGuess": "Cloudflare", "source": "cymru" }
    ],
    "asns": [13335],
    "countries": ["US"],
    "hostingProvider": "Cloudflare"
  },
  "hostingProvider": "Cloudflare",
  "hasSecurityTxt": true
}
```

The values above are illustrative; live records reflect what the registry, resolvers and the site return at run time. The top-level `registrar`, `expirationDate`, `mxProviderGuess`, `dmarcPolicy`, `tlsDaysUntilExpiry`, `httpStatusCode`, `techHints`, `emailSecurityScore` and `summary` columns are flattened copies so the Console table and CSV export stay readable.

Each section has a `status`: `ok`, `error` (reached the source, got a bad answer), `unavailable` (could not reach the source at all — for example a domain with no web server on port 443, or a country-code TLD with no RDAP server) or `skipped` (not requested). When a section fails, `errors` holds the reason, e.g. `{ "tls": "ECONNREFUSED: connect ECONNREFUSED 93.184.216.34:443" }`, and the remaining sections are still returned.

Inputs that are not a domain at all produce `{ "input": "not a domain", "error": "INVALID_DOMAIN", "errorMessage": "..." }` and are charged at the same rate, because the work to classify them was done.

#### Email security score

| Check | Points |
|---|---|
| SPF record present and syntactically valid (≤ 10 lookups, no unknown terms, single record) | 25 |
| DMARC policy `reject` / `quarantine` / `none` | 35 / 20 / 5 |
| At least one DKIM selector found | 25 |
| MTA-STS record present | 10 |
| BIMI record present | 5 |

If the output saves you time, a review on the Store page helps other buyers find this Actor; the Store invites you to leave one after you have run it a few times.

### Alerts and delivery

The Actor has no diff mode: every run returns the full record for every domain, which makes it easy to schedule for expiry and deliverability monitoring and compare the columns you care about downstream.

- **Schedule in the Console.** Save the domain list as a Task, then create a schedule under **Schedules** (weekly is enough for `daysUntilExpiry` and `tlsDaysUntilExpiry`; daily if you watch DMARC or MX changes) and attach the Task.
- **The Actor's own webhook.** Set `webhookUrl` and the Actor POSTs one JSON document when the run finishes (10 second timeout, two retries; a failed delivery is logged and never fails the run). The body is the run summary: `actor`, `runId`, `domains`, `invalidInputs`, `produced`, `sectionErrors`, `timeBudgetDomains`, `charges`, `limitReached`, `finishedAt`. Domain records are not in the body; read them from the dataset, for example with the `fields` parameter to pull only `domain`, `expirationDate`, `tlsDaysUntilExpiry` and `dmarcPolicy`.
- **Apify run webhooks.** On the Actor's **Integrations** tab add a webhook for run events such as `ACTOR.RUN.SUCCEEDED`, `ACTOR.RUN.FAILED` or `ACTOR.RUN.TIMED_OUT`; Apify POSTs the run id and the default dataset id to your URL, which is all your own code needs to fetch the items.
- **Slack, Make, Zapier, Google Sheets, email.** The same **Integrations** tab lists Apify's ready-made integrations: a Slack message when a run finishes, Make and Zapier modules that start runs and read the dataset, an export of each run's dataset to a Google Sheet, and an email notification. They are configured per Actor or per task, without code.
- **API and dataset endpoints.** Start a run with `POST https://api.apify.com/v2/acts/owlabee~spf-dmarc-dkim-checker/runs`, read the newest results with `GET https://api.apify.com/v2/acts/owlabee~spf-dmarc-dkim-checker/runs/last/dataset/items?format=json` (also `csv` or `xlsx`, and `fields` to pick columns), or read any run's dataset at `https://api.apify.com/v2/datasets/<datasetId>/items`. For short runs `run-sync-get-dataset-items` starts the run and returns the items in one call. A Google Sheets export per run plus a sheet formula on `daysUntilExpiry` is a complete expiry alert without code.

### Pricing

Pay per event. You only pay for records the Actor produces; set a **maximum cost per run** in the run options to cap spend.

| Event | Price | Notes |
|---|---|---|
| Domain report (`domain-enriched`) | $0.0015 | Per domain record written to the dataset, including error records; covers the default sections (`whois`, `dns`, `tls`, `http`) whichever of them run |
| Opt-in section check (`section-checked`) | $0.0005 | Per opt-in section (`wellknown`, `ip`) per domain checked, on top of the domain report, whether the section succeeds, fails or times out; never charged for the default sections or for invalid inputs |
| Actor start | $0.00005 | Apify platform event |

**Example:** 1,000 domains ≈ **$1.50** with the default sections; running fewer of them does not change the price per domain, it only makes the run faster. Adding both opt-in sections makes it 1,000 × ($0.0015 + 2 × $0.0005) = **$2.50**; `wellknown` alone ≈ $2.00.

Store pricing discounts: users on the Scale plan (Silver) pay 10% less per event, Business (Gold) 15% less.

### Limits and fair use

- Each domain triggers roughly 37 small requests in the default sections: one RDAP lookup, about 34 DNS-over-HTTPS queries (7 record types, DMARC, MTA-STS, BIMI, TLS-RPT, DNSKEY, DS, TLSA on up to 3 MX hosts, 18 DKIM selectors), one TLS handshake and one HTTP GET (≤ 5 redirects, body capped at 512 KB). Public DNS resolvers are built for this volume; registries and target sites receive at most one request per domain.
- The opt-in `wellknown` section sends 9 small GETs to the domain's own host (robots.txt first, then 8 files, 500 ms apart), skips any path robots.txt disallows, and requests nothing more when robots.txt answers with a server error (RFC 9309 reads that as "disallow everything"). Each file is read up to a few KB (ads.txt and robots.txt up to 16 and 24 KB), 72 KB per domain at most. An HTML page served where a text file belongs is not counted as the file.
- The opt-in `ip` section asks Team Cymru's IP-to-ASN service and reverse DNS through the same DNS-over-HTTPS resolvers (up to 8 queries) and calls RIPEstat only for an address Team Cymru does not answer for. The hosting guess covers well-known clouds, CDNs and hosts and stays `null` for other networks.
- Country-code TLDs: RDAP answers come from the IANA directory plus the registry of .io and .me. Other country codes the directory does not list (for example .us, .de, .ch, .co, .eu, .jp) come back with `whois.status: "unavailable"`; .de, .ch and .li registries publish RDAP but restrict how lookups may be used, so they are not queried until that is reviewed. Identity Digital (.io, .me) rate-limits hard: requests to it are spaced 2 seconds apart, a refusal is not retried, and after one the Actor stops asking it for 60 seconds, so a long list of .io domains may show some `unavailable` WHOIS sections.
- `caaAllowsIssuer` compares the CAA records with the CA of the certificate served today; a certificate issued before the CAA records were set can show `false` without having been misissued. Unknown CAs give `null`.
- Timeouts: up to 15 s for RDAP and HTTP, 8 s per DNS query and for the TLS handshake, always within the domain's 20 s budget. A slow source marks its section `error`/`unavailable`; it never blocks the other sections.
- **Time budget per domain.** All sections of a domain share a 20 second budget and run in parallel; every request and redirect hop is capped by what is left of it, so a dead or silent host is cut off after about 20 seconds instead of waiting out every hop and resolver (the sections that did not finish are marked `error` or `unavailable`). A `Retry-After` longer than 10 seconds is not waited for. The record is still produced and charged like every record; when every requested section failed after the budget ran out it carries `error: "TIME_BUDGET"`, and the run summary counts these domains in `timeBudgetDomains`.
- Default run memory is 512 MB (also the maximum).
- DKIM can only be *discovered*, not enumerated: a domain whose selector is not in the probed list shows `dkimSelectorsFound: []` even though DKIM is configured. Add your selectors in `dkimSelectors`.
- The SPF lookup count covers the top-level record only (includes are not recursed), so it is a lower bound.
- Registrable-domain extraction uses a curated list of common multi-label suffixes (`co.uk`, `com.au`, `github.io`, …) rather than the full Public Suffix List; exotic suffixes may be reduced one label too far.
- Tech hints are header/HTML fingerprints of the landing page only and are meant as a signal, not an audit.
- Subdomains are reduced to their registrable domain (`mail.example.com` → `example.com`). To inspect a specific host's certificate, run the host through a dedicated TLS scanner.

### Related Actors

Other pay-per-event Actors from the same account, all limited-permission and agent-friendly:

- [Website Change Monitor — Sitemap Monitor & Page Diff](https://apify.com/owlabee/website-change-monitor) — watch the pages behind these domains for new, removed and changed content.
- [Shopify Catalog Monitor & Price Tracker — Products Scraper](https://apify.com/owlabee/shopify-catalog-monitor) — when the tech hints say Shopify, pull the store's full catalog or monitor its prices.
- [Hiring Signals & ATS Jobs Scraper — Greenhouse, Lever, Ashby](https://apify.com/owlabee/job-postings-api) — add open roles and hiring velocity from the company's public job board.

### Guide and code examples

- Guide: [Bulk SPF, DMARC and DKIM Checker for Domain Lists](https://owlabee.com/spf-dmarc-dkim-checker/), covering the source, its fields and limits, and curl and MCP examples.
- Runnable examples (curl, Python, Node.js and an MCP config): [owlabee/owlabee on GitHub](https://github.com/owlabee/owlabee/tree/main/examples/spf-dmarc-dkim-checker).

### Use from AI agents (MCP)

Pin this Actor in the Apify MCP server with one link: `https://mcp.apify.com?tools=owlabee/spf-dmarc-dkim-checker`, or let the agent find it with the `search-actors` tool. It is pay-per-event with limited permissions, so agents can discover it, inspect the input schema with `fetch-actor-details` and pay per run through agentic payments. Prompts that map directly onto the input:

1. "Check SPF, DMARC and DKIM for these 50 customer domains and flag any without a DMARC reject policy or with an invalid SPF record."
2. "Which of these competitor domains run on Shopify or WordPress, and which use Google Workspace vs Microsoft 365 for email?"
3. "List the domains in this sheet whose TLS certificate or domain registration expires in the next 30 days, with registrar and issuer."

The agent receives the same dataset records shown in the output example above.

To call it from an agent that already has the Apify MCP server connected, copy this tool call and change only the `input` object (shown with the quick-start input; any input that validates in the Console works here too). The server returns the run's dataset items, and its per-call maximum total charge (`maxTotalChargeUsd`) caps spend the same way the run option does in the Console.

```json
{
  "tool": "call-actor",
  "arguments": {
    "actor": "owlabee/spf-dmarc-dkim-checker",
    "input": {
      "domains": [
        "example.com",
        "apify.com",
        "github.com"
      ],
      "sections": [
        "whois",
        "dns",
        "tls",
        "http"
      ]
    }
  }
}
```

### FAQ

**Does this need proxies or an API key?** No. RDAP, DNS-over-HTTPS (Cloudflare, with Google as fallback), the domains' own public endpoints and, in the `ip` section, Team Cymru's IP-to-ASN service and RIPEstat are all free and open; the Actor runs on Apify's standard datacenter infrastructure.

**How is cost controlled?** Every domain is charged the moment its record is saved, and the Actor stops cleanly when the run's maximum total charge is reached. Deduplication means you never pay twice for `www.example.com` and `example.com`.

**Why is a WHOIS field empty?** Most registries redact personal data under GDPR/ICANN rules, and some ccTLDs do not offer RDAP at all. The Actor reports exactly what the registry returns and never fills gaps with guesses; `whois.source` tells you which server answered. A domain whose TLD has no known RDAP server gets `whois.status: "unavailable"`, never `registered: false`.

**Do the well-known and IP sections cost extra?** Yes, $0.0005 per section per domain on top of the $0.0015 record, because each adds its own requests (about 9 to the domain's own host for `wellknown`, up to 11 lookups for `ip`) and a few seconds per domain. The four default sections are included in the record whichever of them run, so a run that does not add `wellknown` or `ip` costs exactly what it always did.

**Which AI crawlers does the robots.txt check cover?** GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-User, Claude-SearchBot, anthropic-ai, CCBot, Google-Extended, Applebot-Extended, PerplexityBot, Bytespider, Meta-ExternalAgent and Amazonbot. Each gets `blocked` (the whole site), `partial` (some paths), `allowed` or `not-mentioned` (it follows the `User-agent: *` rules).

**Why does `dkimSelectorsFound` say nothing when I know DKIM is set up?** DKIM selectors are not discoverable from the outside; the Actor probes 18 common ones. Add your provider's selector in `dkimSelectors` (for example `fm1`, `fm2`, `fm3` for Fastmail or `krs` for Klaviyo).

**What is `tls.status: "unavailable"`?** Nothing answered on port 443 (no web server, firewall, or DNS does not resolve). It is a finding about the domain, not an Actor error; the other sections are still filled.

**Can an AI agent use this?** Yes; see "Use from AI agents (MCP)" above for the pin link and example prompts. Schedules and webhooks work the same way from the Console or the API.

**Can I try it for free?** Yes. The Apify free plan comes with monthly platform credit, and the quick-start input above costs a few cents per run, so you can test it on your own inputs before paying anything.

**How do I find this Actor again?** Bookmark it on the Store page (the bookmark icon next to the title); bookmarked Actors are listed in your Console, and the Store page shows new versions as they ship.

### Support

Questions, bugs or a source you'd like added? Open an issue on the Actor's **Issues** tab, or email jacob@owlabee.com.

### Changelog

- **2026-11-08** — Pricing: new `section-checked` event ($0.0005; Silver $0.00045, Gold $0.0004) per opt-in section (`wellknown`, `ip`) per domain, on top of the domain report. The default sections and every saved input that does not add the two opt-in sections cost exactly what they did before.
- **2026-11-07** — Wider checks, defaults unchanged. DNS section: TLS-RPT, DNSSEC validation (AD flag, DNSKEY, DS), DANE TLSA on the MX hosts, MTA-STS policy vs MX, CAA issuers, `mailTransportIssues`, and a top-level `caaAllowsIssuer` (the email security score is unchanged). New opt-in sections `wellknown` (security.txt, robots.txt AI-crawler rules, llms.txt, ads.txt, GPC, change-password, OpenID, app links) and `ip` (ASN, network, prefix, country, reverse DNS, hosting guess). WHOIS now also covers .io and .me through their registry. Fix: a TLD without any RDAP server was reported as `registered: false`; it is now `whois.status: "unavailable"`.
- **2026-11-07** — Renamed from `owlabee/domain-intelligence` to `owlabee/spf-dmarc-dkim-checker` so the address matches what people search for. Input, output and pricing are unchanged; calls by Actor ID, saved tasks and schedules are unaffected. Name-based calls to the old address stop resolving: update API URLs and MCP tool lists.
- **2026-10-27** — README: comparison table, ready-to-run inputs for a DNS/email-security audit and a WHOIS-only check, alerts and delivery, MCP call example, changelog.
- **2026-10-25** — Pricing round: event title pluralized and Store discount tiers for the Scale and Business plans described; applied in the Console on 2026-10-26.
- **2026-10-23** — 20 second time budget per domain shared by all sections, every request and redirect hop capped by the remaining budget, `TIME_BUDGET` marker and `timeBudgetDomains` count, worst-case margin tests. Listing: new title, SEO fields, MCP section, quick start, task examples; Related Actors use the new display titles.
- **2026-10-17** — Public task examples published next to the Actor.
- **2026-10-16** — First release on the Store: RDAP, DNS and email security score, TLS, HTTP headers and tech hints, flattened columns.

### Legal

The Actor reads only public registration data (RDAP), public DNS, public TLS certificates and the public landing page of each domain, at a rate of one request per domain per source. Use the data in compliance with the registries' terms and applicable law. This Actor is not a consumer reporting agency and its output must not be used for employment, credit, insurance, or tenant screening decisions.

# Actor input Schema

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

Domains to check, one per line. Full URLs (https://www.example.com/page) and email addresses (jane@example.com) are accepted; the registrable domain is extracted and duplicates are merged. The prefilled values are the cheap, always-available examples used by Apify's daily automated test.

## `sections` (type: `array`):

Which checks to run for every domain. WHOIS = RDAP registration data (IANA bootstrap plus verified registry servers for .io and .me); DNS = records plus SPF/DMARC/DKIM/MTA-STS/BIMI and the email security score, TLS-RPT, DNSSEC validation, DANE TLSA on the MX hosts, MTA-STS policy vs MX and CAA; TLS = certificate on port 443; HTTP = redirects, security headers and tech stack hints. Opt-in, not in the default: Well-known files = security.txt, robots.txt AI-crawler rules, llms.txt, ads.txt, GPC, change-password, OpenID and app links (about 9 small requests to the domain's own host); IP & hosting = ASN, network name, prefix, country, reverse DNS and a hosting/CDN guess for up to 3 addresses (Team Cymru over DNS, RIPEstat as fallback). Fewer default sections = faster runs at the same price per domain; each of the two opt-in sections adds $0.0005 per domain on top of the domain report.

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

Additional DKIM selectors to probe on top of the built-in list (google, default, selector1, selector2, k1, k2, k3, mail, s1, s2, dkim, mandrill, mailgun, sendgrid, amazonses, zoho, pm, protonmail). Each selector costs one DNS query per domain.

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

How many domains are checked in parallel. 5 is polite and fast enough for a few thousand domains; raise it for large lists.

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

If set, the Actor POSTs a JSON summary of the run (counts, charges, finish time) to this URL when it finishes. Failures are logged, never fatal.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "apify.com",
    "github.com"
  ],
  "sections": [
    "whois",
    "dns",
    "tls",
    "http"
  ],
  "dkimSelectors": [],
  "maxConcurrency": 5
}
```

# Actor output Schema

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

One item per domain in the default dataset (domain, registrar, expirationDate, mxProviderGuess, dmarcPolicy, emailSecurityScore, tlsDaysUntilExpiry, httpStatusCode, techHints, summary; error items carry error and errorMessage). Fields are described in dataset_schema.json.

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

SUMMARY record in the run's default key-value store: domains processed, records produced and charges, written when the run finishes.

# 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",
        "apify.com",
        "github.com"
    ],
    "sections": [
        "whois",
        "dns",
        "tls",
        "http"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("owlabee/spf-dmarc-dkim-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": [
        "example.com",
        "apify.com",
        "github.com",
    ],
    "sections": [
        "whois",
        "dns",
        "tls",
        "http",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("owlabee/spf-dmarc-dkim-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": [
    "example.com",
    "apify.com",
    "github.com"
  ],
  "sections": [
    "whois",
    "dns",
    "tls",
    "http"
  ]
}' |
apify call owlabee/spf-dmarc-dkim-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,owlabee/spf-dmarc-dkim-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/UhyhEXvAqRIyl6SAt/builds/48Z3h60rWtwYYx85f/openapi.json
