Bulk Email Health: SPF, DMARC, DKIM & MX Checker avatar

Bulk Email Health: SPF, DMARC, DKIM & MX Checker

Pricing

from $2.00 / 1,000 mail health reports

Go to Apify Store
Bulk Email Health: SPF, DMARC, DKIM & MX Checker

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

cottony_brittlegill

Maintained by Community

Actor 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) or p=reject (refuse outright). p=none is 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:

FieldTypeMeaning
domainstringNormalized hostname you queried (punycode if IDN)
registrableDomainstringeTLD+1 actually queried
statusanalyzed | no_mail | errorSee "Honest limits" below
mx.present / mx.hostsboolean / arrayWhether the domain publishes MX records, and which hosts
spfobject | nullSPF presence, qualifier (fail/softfail/neutral/pass/null), includes, dnsLookups, issues. null only if the DNS read itself errored
dmarcobject | nullDMARC presence, policy, subdomainPolicy, pct, rua, ruf, issues. null only if the DNS read itself errored
dkimobject | nullselectorsTried, selectorsFound, selectorsErrored, present, issues. null when DKIM probing is turned off
gradeA-F | nullSee "Grade & score" below. null only when status is error
score0-100 | nullSame as above
issuesstring[]Aggregated, human-readable, actionable (deduplicated)
errorsstring[]Non-fatal DNS/lookup errors (technical, not a verdict)
checkedAtISO dateWhen the check ran
chargeablebooleantrue unless status is error
chargedbooleanWhether 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 is COMMON_SELECTORS in src/core/dkim.ts, and every report's own dkim.selectorsTried echoes exactly what was probed for that domain, so it can never drift out of sync with what actually ran), or your own list via the dkimSelectors input.
    dkim.present: false
    means "not found at the selectors we tried" — never "this domain has no DKIM." The google.com example 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 domains and domainsText combined) 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), status is "error", grade/score are null, 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_mail is not a failure. A domain that simply doesn't receive email (no MX records) gets status: "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 publish v=spf1 -all and a DMARC reject policy 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/redirect mechanisms in the record itself and flag when they exceed 10 (the PermError threshold), but we do not recursively resolve nested include: 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:

EventPriceWhen it fires
actor-start$0.005Once per run
mail-report$0.002Once 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
}
FieldTypeDefaultDescription
domainsstring[]["google.com", "github.com"]Domains to check (max 100,000). Bare domains or full URLs; scheme/www/path stripped automatically.
domainsTextstringAlternative/additional domains pasted as newline- or comma-separated text; merged with domains.
dkimbooleantrueProbe common DKIM selectors. Turning this off skips ~18 sequential DNS lookups per domain (faster, but no dkim data — reports come back with dkim: null).
dkimSelectorsstring[]Optional: probe your own selector list instead of the 18 common defaults (useful if you know your ESP's selector).
globalConcurrencyinteger20Concurrent domains in flight.
lookupTimeoutMsinteger10000Per-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.

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.