Bulk Email Health: SPF, DMARC, DKIM & MX Checker
Pricing
from $2.00 / 1,000 mail health reports
Bulk Email Health: SPF, DMARC, DKIM & MX Checker
Bulk email deliverability and security audit: MX, SPF, DMARC and DKIM (common selectors) for up to 100,000 domains in one normalized JSON per domain, with an A-F grade and actionable issues. Failed lookups are never charged.
Pricing
from $2.00 / 1,000 mail health reports
Rating
0.0
(0)
Developer
cottony_brittlegill
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
15 hours ago
Last modified
Categories
Share
Domain Mail-Health Bulk — SPF, DMARC & DKIM Checker at Scale
Bulk email deliverability and anti-spoofing audit: MX, SPF, DMARC and DKIM
(common selectors) for up to 100,000 domains in one run — one normalized
JSON per domain, with an A-F grade and a list of actionable issues. A bulk
SPF checker, a DMARC lookup tool and a DKIM record checker combined, so you
don't have to run dig three times per domain and reconcile the answers by
hand.
The three records, briefly (for non-experts)
- SPF (Sender Policy Framework) — a DNS TXT record listing which mail servers are allowed to send email "from" your domain. Without it, anyone can forge an email that claims to be from you.
- DMARC — a DNS TXT record (at
_dmarc.<domain>) that tells receiving mail servers what to do with messages that fail SPF/DKIM checks:p=none(monitor only),p=quarantine(send to spam) orp=reject(refuse outright).p=noneis the common half-configured state: DMARC is published, but it enforces nothing yet. - DKIM — a cryptographic signature added to outgoing mail, verified via
a DNS TXT record at
<selector>._domainkey.<domain>. The<selector>is chosen by whoever sends your mail (Google, Microsoft, your ESP...) and isn't discoverable from DNS alone — see "Honest limits" below.
Together, a domain with a strict SPF, a p=reject DMARC and a valid DKIM
key is hard to spoof. Missing any one of them leaves a real phishing/spoofing
gap — this actor tells you exactly which one, per domain.
What you get per domain
Every dataset item is a single MailHealthReport:
| Field | Type | Meaning |
|---|---|---|
domain | string | Normalized hostname you queried (punycode if IDN) |
registrableDomain | string | eTLD+1 actually queried |
status | analyzed | no_mail | error | See "Honest limits" below |
mx.present / mx.hosts | boolean / array | Whether the domain publishes MX records, and which hosts |
spf | object | null | SPF presence, qualifier (fail/softfail/neutral/pass/null), includes, dnsLookups, issues. null only if the DNS read itself errored |
dmarc | object | null | DMARC presence, policy, subdomainPolicy, pct, rua, ruf, issues. null only if the DNS read itself errored |
dkim | object | null | selectorsTried, selectorsFound, selectorsErrored, present, issues. null when DKIM probing is turned off |
grade | A-F | null | See "Grade & score" below. null only when status is error |
score | 0-100 | null | Same as above |
issues | string[] | Aggregated, human-readable, actionable (deduplicated) |
errors | string[] | Non-fatal DNS/lookup errors (technical, not a verdict) |
checkedAt | ISO date | When the check ran |
chargeable | boolean | true unless status is error |
charged | boolean | Whether the Apify platform actually billed this report (see Pricing) |
Real output for google.com from a live run (dates/exact selectors will
differ on your own run):
{"domain": "google.com","registrableDomain": "google.com","status": "analyzed","mx": {"present": true,"hosts": [{ "priority": 10, "exchange": "smtp.google.com" }],"error": null},"spf": {"present": true,"raw": "v=spf1 include:_spf.google.com ~all","allQualifier": "softfail","includes": ["_spf.google.com"],"dnsLookups": 1,"issues": []},"dmarc": {"present": true,"raw": "v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com","policy": "reject","subdomainPolicy": null,"pct": null,"rua": ["mailto:mailauth-reports@google.com"],"ruf": [],"issues": []},"dkim": {"selectorsTried": ["google", "selector1", "selector2", "default", "k1", "k2", "dkim", "mail", "s1", "s2", "mandrill", "mxvault", "zoho", "fm1", "fm2", "fm3", "protonmail", "pm"],"selectorsFound": [],"selectorsErrored": [],"present": false,"issues": ["No DKIM key found at common selectors (the domain may use a custom selector we did not probe)"]},"grade": "B","score": 82,"issues": ["No DKIM key found at common selectors (the domain may use a custom selector we did not probe)"],"errors": [],"checkedAt": "2026-07-28T21:24:29.018Z","chargeable": true,"charged": false}
Notice google.com grades B, not A — this is the honest DKIM limitation
in action (see below), not a bug: Google signs its own mail with a rotating,
dated selector (not one of the 18 common defaults we probe), so dkim.present
correctly comes back false even though Google obviously does sign its mail.
The ~all SPF qualifier (softfail, not the strictest -all) also costs a
few points. Both are real, defensible signals — not false negatives.
Grade & score — a v1 heuristic, not a certification
score starts at 100 and subtracts fixed penalties per dimension (no SPF
-30, SPF +all (permissive "pass everything") -25, SPF with no all
mechanism or a neutral ?all -12, no DMARC -30, p=none -18,
p=quarantine -8, no DKIM at common selectors -15, multiple/duplicate
records -12 to -25, missing DMARC aggregate reports -5, partial pct
enforcement -5, and a few more — see src/core/report.ts for the exact
table). grade maps the score to A (≥90) / B (≥75) / C (≥60) / D (≥40) / F
(below). These weights are our v1 judgment call, not an industry
standard or a certification — treat the grade as a prioritization signal
("fix this domain first"), not a compliance attestation.
Cap-at-C rule: whenever a dimension could not actually be checked — the SPF or DMARC DNS read itself errored, or even one of the (up to 18) DKIM selector lookups errored — that dimension contributes no penalty of its own, but the final grade is capped at C. This is deliberately conservative, not a bug: a selector lookup that errored is a selector we never got a real answer for, not one we verified as absent, so we treat that DKIM check as unreliable rather than assume the failed lookup would have come back clean. A single flaky DNS answer among 18 selector lookups is enough to withhold a top grade — we would rather under-grade a domain on incomplete evidence than hand out a false A or B. The cap never upgrades a grade: a domain that already scores D or F on the dimensions we could check stays D or F.
Honest limits (read before buying)
- DKIM: common-selector probing, not proof of absence. There is no DNS
mechanism to enumerate which selector(s) a domain uses for DKIM signing —
you can only check
<selector>._domainkey.<domain>for a selector you already know. We probe 18 widely-used default selectors (google,selector1,selector2,default,k1/k2,dkim,mail,s1/s2,mandrill,mxvault,zoho,fm1-fm3,protonmail,pm— the definitive, current list isCOMMON_SELECTORSinsrc/core/dkim.ts, and every report's owndkim.selectorsTriedechoes exactly what was probed for that domain, so it can never drift out of sync with what actually ran), or your own list via thedkimSelectorsinput.means "not found at the selectors we tried" — never "this domain has no DKIM." Thedkim.present: falsegoogle.comexample above is a real illustration: Google signs its own mail, but with a selector outside our default list. - The 100,000-domain cap truncates, it doesn't error. Submit more than
100,000 domains in one run (across
domainsanddomainsTextcombined) and only the first 100,000 raw entries are looked at — anything past that is dropped before any DNS lookup, with no dataset row and no error. This is never silent: whenever it happens, the run log carries an explicit warning stating exactly how many domains were submitted, processed and dropped. Split larger lists across multiple runs instead. - Dataset row count can be lower than your input list. Two things
reduce it, neither of which is an error: exact-duplicate domains are
deduplicated to a single report, and malformed entries (not a real
domain) are dropped entirely — logged as
Invalid domain skipped: ...in the run log, never written to the dataset. If you're reconciling row counts against your input, check the run log rather than assuming a 1:1 mapping. - A DNS error is reported as
error, never as a false "insecure". If the MX lookup itself fails (timeout, SERVFAIL, etc. — anything other than the benign "no record" answer),statusis"error",grade/scorearenull, and the report is not charged (chargeable: false). We never turn an unreliable DNS answer into a low grade — that would misrepresent a network hiccup as a security failing. no_mailis not a failure. A domain that simply doesn't receive email (no MX records) getsstatus: "no_mail"— IS charged (the lookup succeeded and the data is reliable), and still gets SPF/DMARC/DKIM checked — best practice is for such a domain to publishv=spf1 -alland a DMARCrejectpolicy specifically to prevent anyone spoofing mail "from" a domain that never sends any.- SPF's DNS-lookup counting is a heuristic, not a full RFC 7208
resolver: we count
include/a/mx/ptr/exists/redirectmechanisms in the record itself and flag when they exceed 10 (the PermError threshold), but we do not recursively resolve nestedinclude:chains to get an exact final count. - No mailbox-level testing. This is a DNS-record audit (what you publish), not a live send test (whether a message actually lands in the inbox) — spam-filter reputation, content filtering and IP reputation are outside this actor's scope.
Pricing
Pay-Per-Event (PPE) — no subscription:
| Event | Price | When it fires |
|---|---|---|
actor-start | $0.005 | Once per run |
mail-report | $0.002 | Once per domain report with chargeable: true (status other than error) |
$2.00 per 1,000 domains (mail-report events) plus a flat $0.005 per
run. Domains whose lookup itself failed (status: "error") are never
billed — chargeable/charged are pushed on every dataset item so you can
reconcile your own invoice against the data.
Use cases
1. Deliverability audit of a prospect list. Before a cold-email campaign, check every domain in your prospect/lead list for MX/SPF/DMARC — domains without a receiving mailbox or with a hostile DMARC policy waste send reputation; skip or deprioritize them.
2. Vendor/supplier due-diligence. Part of a security or compliance
review: does this vendor's domain enforce DMARC reject? No DKIM at all?
A weak mail posture on a domain you're about to exchange sensitive data
with is a real signal, cheap to check in bulk before onboarding.
3. Monitoring your own domain fleet. Companies with dozens of marketing/regional/legacy domains routinely forget to lock down SPF/DMARC on domains that don't send mail — exactly the domains attackers spoof. Run this on your whole portfolio on a schedule and catch drift.
Standard Apify API call (synchronous, returns the dataset items directly):
curl -X POST \"https://api.apify.com/v2/acts/YOUR_USERNAME~domain-mailhealth-bulk/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"domains": ["google.com", "freewebs.com", "test.com"],"dkim": true}'
FAQ
How do I check SPF, DMARC and DKIM for a list of domains at once?
Feed the actor a list (or newline/comma-separated text) of domains; it
checks MX, SPF, DMARC and 18 common DKIM selectors per domain in one run
and returns a normalized JSON with an A-F grade and specific issues per
domain — no need to run dig/checkdmarc once per domain and reconcile
the answers yourself.
Can you check DKIM without knowing the selector?
Not really — there's no DNS mechanism to enumerate which selector a domain
uses for signing. We probe 18 widely-used default selectors (or your own
list via dkimSelectors); dkim.present: false means "not found at the
selectors we tried," never "this domain has no DKIM."
Why doesn't google.com get an A grade from this checker?
Because Google signs its mail with a rotating, dated selector outside our
18 common defaults, so dkim.present correctly comes back false even
though Google obviously signs its mail — an honest "not found at the
selectors we tried" result, not a bug. Its ~all SPF softfail (rather than
the stricter -all) also costs a few points. See the worked example above.
What does DMARC p=none mean, and is it enough?
p=none means DMARC is published but enforces nothing — messages that fail
SPF/DKIM still get delivered, just reported on. p=quarantine sends
failures to spam, p=reject refuses them outright. p=none is the common
half-configured state this actor flags as an issue, not a pass.
Input / Output
Input
{"domains": ["google.com", "github.com"],"domainsText": "","dkim": true,"dkimSelectors": [],"globalConcurrency": 20,"lookupTimeoutMs": 10000}
| Field | Type | Default | Description |
|---|---|---|---|
domains | string[] | ["google.com", "github.com"] | Domains to check (max 100,000). Bare domains or full URLs; scheme/www/path stripped automatically. |
domainsText | string | — | Alternative/additional domains pasted as newline- or comma-separated text; merged with domains. |
dkim | boolean | true | Probe common DKIM selectors. Turning this off skips ~18 sequential DNS lookups per domain (faster, but no dkim data — reports come back with dkim: null). |
dkimSelectors | string[] | — | Optional: probe your own selector list instead of the 18 common defaults (useful if you know your ESP's selector). |
globalConcurrency | integer | 20 | Concurrent domains in flight. |
lookupTimeoutMs | integer | 10000 | Per-lookup timeout in milliseconds (applies to every individual MX/TXT query). |
Output (dataset items)
One MailHealthReport per domain (see field table above), in the order
resolved (not necessarily input order, since domains run concurrently).
Three real examples from a live run — a well-configured domain, a domain
that receives mail but publishes no auth records at all, and a domain with
no mail setup whatsoever:
{"domain": "freewebs.com","registrableDomain": "freewebs.com","status": "analyzed","mx": {"present": true,"hosts": [{ "priority": 0, "exchange": "eu-smtp-inbound-1.mimecast.com" },{ "priority": 5, "exchange": "eu-smtp-inbound-2.mimecast.com" }],"error": null},"spf": { "present": false, "raw": null, "allQualifier": null, "includes": [], "dnsLookups": 0,"issues": ["No SPF record: sender spoofing is not mitigated"] },"dmarc": { "present": false, "raw": null, "policy": null, "subdomainPolicy": null, "pct": null, "rua": [], "ruf": [],"issues": ["No DMARC record: no domain-level email authentication enforcement"] },"dkim": {"selectorsTried": ["google", "selector1", "selector2", "default", "k1", "k2", "dkim", "mail", "s1", "s2", "mandrill", "mxvault", "zoho", "fm1", "fm2", "fm3", "protonmail", "pm"],"selectorsFound": [], "selectorsErrored": [], "present": false,"issues": ["No DKIM key found at common selectors (the domain may use a custom selector we did not probe)"]},"grade": "F","score": 25,"issues": ["No SPF record: sender spoofing is not mitigated","No DMARC record: no domain-level email authentication enforcement","No DKIM key found at common selectors (the domain may use a custom selector we did not probe)"],"errors": [],"checkedAt": "2026-07-28T21:24:28.776Z","chargeable": true,"charged": false}
{"domain": "test.com","registrableDomain": "test.com","status": "no_mail","mx": { "present": false, "hosts": [], "error": null },"spf": { "present": false, "raw": null, "allQualifier": null, "includes": [], "dnsLookups": 0,"issues": ["No SPF record: sender spoofing is not mitigated"] },"dmarc": { "present": false, "raw": null, "policy": null, "subdomainPolicy": null, "pct": null, "rua": [], "ruf": [],"issues": ["No DMARC record: no domain-level email authentication enforcement"] },"dkim": {"selectorsTried": ["google", "selector1", "selector2", "default", "k1", "k2", "dkim", "mail", "s1", "s2", "mandrill", "mxvault", "zoho", "fm1", "fm2", "fm3", "protonmail", "pm"],"selectorsFound": [], "selectorsErrored": [], "present": false,"issues": ["No DKIM key found at common selectors (the domain may use a custom selector we did not probe)"]},"grade": "F","score": 25,"issues": ["Domain has no MX records (does not receive email); it should still publish SPF -all and DMARC reject to prevent spoofing","No SPF record: sender spoofing is not mitigated","No DMARC record: no domain-level email authentication enforcement","No DKIM key found at common selectors (the domain may use a custom selector we did not probe)"],"errors": [],"checkedAt": "2026-07-28T21:24:31.136Z","chargeable": true,"charged": false}
Both examples above are chargeable: true (charged: false only because
this was a local run outside Apify's Pay-Per-Event ledger, not because the
result was unreliable) — a low grade or no_mail status is a legitimate,
billable result. Only genuine DNS-lookup failures (status: "error") are
chargeable: false.
Related tools
Part of the same domain-tooling family, same pay-per-event pricing, same "never charge a failed lookup" policy:
- Bulk Domain Intelligence — WHOIS/RDAP registration data, DNS records, SSL certificate check and availability. Run it against the same domain list for a full registration + DNS + SSL + mail-security picture; use it when the question is "who owns this and is it up," not "can its mail be trusted."
- Typosquat & Lookalike Domain Detector — finds lookalike/typosquatted domains registered against a brand you own and classifies which look like active threats versus parked inventory. Use it when you're hunting for impersonators of your brand rather than auditing mail security on domains you already have in hand.