# SPF, DKIM & DMARC Checker: Bulk Domain Email Health Score (`lyfe_tools/domain-email-health-check`) Actor

SPF, DKIM and DMARC checker for domains in bulk: MX, SPF (with the 10-lookup limit), DMARC, DKIM, MTA-STS, TLS-RPT and BIMI, scored 0-100 with a concrete fix for each problem. Pay per domain.

- **URL**: https://apify.com/lyfe_tools/domain-email-health-check.md
- **Developed by:** [LYFE Offshore](https://apify.com/lyfe_tools) (community)
- **Categories:** Developer tools, Lead generation, MCP servers
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.75 / 1,000 domain checkeds

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, DKIM & DMARC Checker: Bulk Domain Email Health Score

Bulk SPF, DKIM and DMARC checker for email deliverability: give it a list of domains and it checks the email setup
in DNS and returns a **score from 0 to 100, what is wrong, and the fix**: MX, SPF (including the 10-lookup limit
that quietly breaks big records), DMARC, DKIM, MTA-STS, TLS-RPT and BIMI. Only DNS, plus one request to the
domain's own MTA-STS policy file. No third-party service, nothing that can break when a vendor changes its site.

### One run, real output

Eight domains, run locally on 4 October 2026 (DNS answers change; yours will be current):

| Domain | Status | Score | Mail host | SPF ends | SPF lookups | DMARC | DKIM keys found | MTA-STS |
|---|---|---|---|---|---|---|---|---|
| google.com | checked | 93 | Google Workspace | ~all | 1 | reject | none of the 28 tried | enforce |
| github.com | checked | 84 | Microsoft 365 | ~all | 10 of 10 | quarantine | google, selector1, k1, k2, k3, s1, s2, cm | - |
| stripe.com | checked | 83 | Google Workspace | ~all | 7 | reject | google, s1, s2, mandrill, cm | - |
| shopify.com | checked | 86 | Google Workspace | ~all | 4 | reject | google, mail | - |
| mozilla.org | checked | 86 | Google Workspace | ~all | 7 | reject | google | - |
| lyfeoffshore.com | checked | **57** | Google Workspace | ~all | 1 | **none** | google | - |
| example.com | null_mx | 84 | none (null MX) | -all | 0 | reject | revoked keys only | - |
| notarealdomain-xyz-12345.com | no_domain | - | - | - | - | - | - | - |

The 7 domains that exist were billed; the made-up one was free. The row for `lyfeoffshore.com` carries the
finding and the fix, not just a number (see the output example below).

`stripe.com` gets a warning of its own: its strongest DKIM key is 1024 bits, with the fix "Rotate to a 2048-bit key".
`github.com` sits at exactly 10 of 10 SPF lookups: one more include from any provider and SPF breaks, and the row says so.

### What it checks

| Check | What you get |
|---|---|
| **MX** | Records by priority, mail provider recognised from the MX hosts (Google Workspace, Microsoft 365, Proton, Zoho, Mimecast, Proofpoint, Fastmail and others), every MX host must itself resolve, null MX (RFC 7505) recognised, implicit-MX fallback flagged |
| **SPF** | Record fetched and parsed. **DNS lookups counted through every include and redirect** against the limit of 10 (RFC 7208). Flags `+all`, `?all`, a missing `all`, two SPF records, dead or looping includes, the `ptr` mechanism and syntax errors |
| **DMARC** | `p`, `sp`, `pct`, `rua`, `ruf`, alignment. A subdomain without its own record is scored on its parent's policy and says so |
| **DKIM** | 28 common selectors tried (`google`, `selector1`, `selector2`, `default`, `k1`, `s1`, ...) plus your own. Key size read from the key; revoked keys and testing mode (`t=y`) reported. Wildcard `*._domainkey` records are not mistaken for real selectors |
| **MTA-STS** | DNS record, and the policy file fetched from `https://mta-sts.<domain>/.well-known/mta-sts.txt` to read the mode (`enforce`, `testing`, `none`) |
| **TLS-RPT, BIMI** | DNS records |

**The score.** MX 20, SPF 30, DMARC 30, DKIM 10, and 10 for hardening (MTA-STS, TLS-RPT, BIMI),
shown as a percentage. A good setup without MTA-STS and BIMI lands in the high 80s, which is
fine; the hardening is a bonus. Every point lost shows up as a finding with `severity`
(`critical`, `warning`, `info`, `ok`), an explanation and a concrete fix, sorted worst first.

### What it does not do

- **It checks the domain's configuration, not whether a mailbox exists.** `info@company.com` can sit
  on a perfect domain and bounce. For that, use the
  [Catch-All Email Verifier](https://apify.com/lyfe_tools/catchall-email-verifier).
- **DKIM cannot be fully discovered.** DNS has no list of selectors, and a sender can name its
  selector anything. A row with no DKIM key found means none of the selectors tried exist, not that
  DKIM is absent (`google.com` above is the example: none of the 28 selectors exist there, which says nothing
  about whether it signs mail). When no key is found, DKIM is **left out of the score** instead of counted as a failure,
  and `scoreBasis` says so. Add your provider's selector in `dkimSelectors` to check it exactly.
- **No blocklist (DNSBL) check, on purpose.** Spamhaus and similar lists refuse queries that come
  through public resolvers (measured on 4 October 2026 with Spamhaus' own test entry: no answer, or
  "query refused"). A check built on that would report every domain as clean.
- It does not send mail, does not probe SMTP ports (Apify blocks them) and does not read DMARC reports.
- DMARC for a subdomain is looked up on the parent by trimming labels; for domains under public
  suffixes like `co.uk` check the registered domain itself.

### Input example

```json
{ "domains": ["google.com", "https://stripe.com/pricing", "info@shopify.com"],
  "dkimSelectors": ["mycompany1"], "checkMtaStsPolicy": true, "concurrency": 10 }
```

Domains, URLs and email addresses all work: the domain is taken from them. Duplicates are checked once. You can
also point `inputDatasetId` at a dataset from another Actor; the domain is read from `domainField`, or else the
first of `domain`, `website`, `url`, `email`.

### Output example

One row per domain. A real row for `lyfeoffshore.com` from a local run on 5 October 2026, with the per-mechanism
detail objects (`mx`, `spf`, `dmarc`, `dkim`, `mtaSts`, `tlsRpt`, `bimi`) left out for length and the findings
trimmed to two of four:

```json
{
  "domain": "lyfeoffshore.com", "checkedAt": "2026-10-05T17:20:47.205Z",
  "status": "checked", "billable": true, "score": 57, "scoreBasis": "all checks",
  "mxProvider": "Google Workspace", "spfAll": "softfail", "spfLookups": 1,
  "dmarcPolicy": "none", "dkimSelectors": ["google"], "mtaStsMode": null,
  "topIssue": "No DMARC record. Anyone can spoof this domain without a policy to stop them, and Gmail and Yahoo require DMARC for bulk senders.",
  "findings": [
    { "severity": "critical", "area": "dmarc", "message": "No DMARC record. ...",
      "fix": "Publish a TXT record at _dmarc with \"v=DMARC1; p=none; rua=mailto:dmarc@<your domain>\", then tighten the policy." },
    { "severity": "ok", "area": "dkim", "message": "DKIM key found for selector google." }
  ]
}
```

| Field | Meaning |
|---|---|
| `score` | 0 to 100, see "The score" above; `null` for domains that were not checked |
| `scoreBasis` | What the score covers (for example, whether DKIM was left out because no key was found) |
| `status` / `billable` | `checked`, `null_mx` (receives no mail; still scored), `no_domain`, `invalid_input`, `dns_error`; whether the row was charged |
| `mxProvider` | Mail provider recognised from the MX hosts |
| `spfAll` / `spfLookups` | How the SPF record ends, and DNS lookups counted through includes (limit 10) |
| `dmarcPolicy` | `none`, `quarantine` or `reject` |
| `dkimSelectors` | Selectors where a DKIM key was found |
| `mtaStsMode` | `enforce`, `testing`, `none`, or `null` without MTA-STS |
| `topIssue` | The worst finding in one line |
| `findings` | Every finding with `severity`, `area`, `message` and `fix`, worst first |
| `mx`, `spf`, `dmarc`, `dkim`, `mtaSts`, `tlsRpt`, `bimi` | Full detail per mechanism |

A `SUMMARY` record holds the run totals (average score, domains without DMARC, SPF over the lookup limit).

### Price

Price per 1,000 by your Apify plan (Apify applies the right column automatically):

| | Free | Starter | Scale | Business and up |
|---|---|---|---|---|
| Domain checked, per 1,000 | $2.00 | $1.40 | $1.00 | $0.75 |

No start fee. The figures below use the Free-plan price; on a paid plan the same run costs less.

Pay per event: **$0.002 per domain checked**, platform usage included, no start fee.

| Domains | Cost |
|---|---|
| 1,000 | $2.00 |
| 10,000 | $20.00 |

Free: duplicates, invalid input, domains that do not exist, and domains where the resolver did not answer
(`dns_error`). If DNS fails for every domain in a run, the run fails instead of finishing with empty rows. A run's
maximum-charge limit caps how many domains are processed.

### Use it from code or an AI agent

```bash
curl -X POST "https://api.apify.com/v2/acts/lyfe_tools~domain-email-health-check/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["lyfeoffshore.com", "stripe.com"]}'
```

The response is the dataset rows as a JSON array. The Actor also works through the Apify MCP server
(https://mcp.apify.com), so AI agents such as Claude or Cursor can call it as a tool: an agent sends `domains` and
reads `score`, `topIssue` and `findings[].fix`.

### Part of a lead-list workflow

1. Find: [Local Business Leads](https://apify.com/lyfe_tools/osm-business-email-leads) (role addresses such as info@, by city and category)
2. Clean: [Email List Cleaner](https://apify.com/lyfe_tools/email-list-cleaner)
3. Verify: [Catch-All Email Verifier](https://apify.com/lyfe_tools/catchall-email-verifier)
4. **Check the domain: SPF, DKIM & DMARC Checker (this Actor)**

Use it on your own sending domain before a campaign: missing DMARC or a broken SPF record hurts deliverability no
matter how clean the list is. Prospects' domains can be checked too, as a signal (which mail provider, whether
DMARC exists), not as proof that an address works. `inputDatasetId` reads the previous step's dataset, taking the
domain from its `domain` or `email` column.

Next step: this is the last check in the flow. Fix any `critical` finding on your sending domain before you send.

### FAQ

**How do I check SPF, DKIM and DMARC for many domains at once?**
Put the domains (or URLs, or email addresses) in `domains`, or pass a dataset in `inputDatasetId`. Each unique
domain gets one row.

**Does it count SPF DNS lookups (the 10-lookup limit)?**
Yes, through every include and redirect, and it flags records at or over the limit.

**Why does it say no DKIM key was found when my domain signs mail?**
Selectors cannot be listed in DNS. Add your provider's selector in `dkimSelectors`; without a found key, DKIM is
left out of the score.

**Does it check blacklists (DNSBL)?**
No, see "What it does not do".

### Sources and limits

Public DNS only, about 35 queries per domain (28 of them DKIM selectors), plus one HTTPS request to `mta-sts.<domain>`
when it publishes an MTA-STS record. Results are a snapshot of DNS at the moment of the run. Questions and
problems: tools@lyfeoffshore.com.

# Actor input Schema

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

Domains to check (example.com). Full URLs and email addresses work too: the domain is taken from them. Duplicates are checked once.

## `inputDatasetId` (type: `string`):

Read the domains from an existing dataset instead of the list above.

## `domainField` (type: `string`):

Optional. Default: the first of domain, website, url, email.

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

DKIM selectors cannot be listed in DNS, so the Actor tries 28 common ones (google, selector1, selector2, default, k1, s1, ...). If your mail provider gave you another selector name, add it here and it is looked up on every domain first.

## `skipCommonSelectors` (type: `boolean`):

Skip the 28 common selectors and look up only the ones above. Faster.

## `checkMtaStsPolicy` (type: `boolean`):

When a domain publishes an MTA-STS DNS record, fetch https://mta-sts.<domain>/.well-known/mta-sts.txt (one request to the domain itself) to read the mode. Turn off for DNS only.

## `concurrency` (type: `integer`):

How many domains are checked at once.

## `dnsServers` (type: `array`):

Optional resolver IPs (for example 1.1.1.1). Default: the platform's resolver.

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "github.com",
    "stripe.com",
    "shopify.com",
    "mozilla.org",
    "lyfeoffshore.com",
    "example.com",
    "notarealdomain-xyz-12345.com"
  ],
  "skipCommonSelectors": false,
  "checkMtaStsPolicy": true,
  "concurrency": 10
}
```

# Actor output Schema

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

Score 0-100, per-mechanism details and a list of findings with fixes, per domain.

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

Domains checked, average score, how many lack DMARC or break the SPF lookup limit.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "google.com",
        "github.com",
        "stripe.com",
        "shopify.com",
        "mozilla.org",
        "lyfeoffshore.com",
        "example.com",
        "notarealdomain-xyz-12345.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lyfe_tools/domain-email-health-check").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "domains": [
        "google.com",
        "github.com",
        "stripe.com",
        "shopify.com",
        "mozilla.org",
        "lyfeoffshore.com",
        "example.com",
        "notarealdomain-xyz-12345.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("lyfe_tools/domain-email-health-check").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": [
    "google.com",
    "github.com",
    "stripe.com",
    "shopify.com",
    "mozilla.org",
    "lyfeoffshore.com",
    "example.com",
    "notarealdomain-xyz-12345.com"
  ]
}' |
apify call lyfe_tools/domain-email-health-check --silent --output-dataset

```

## MCP server setup

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

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/uyIYBr11yw2W6Wtcf/builds/YNDgScxpC8ulhBzKZ/openapi.json
