# Bulk DMARC, SPF & DKIM Checker (`tildekai/bulk-dmarc-spf-dkim-checker`) Actor

Email security of many domains from public DNS: can the domain be spoofed, Gmail/Yahoo/Microsoft bulk sender readiness, SPF, DMARC, DKIM, MTA-STS, BIMI, DNSSEC, blocklists and lookalikes. Grade, fixes and sales hook.

- **URL**: https://apify.com/tildekai/bulk-dmarc-spf-dkim-checker.md
- **Developed by:** [Attila Kis](https://apify.com/tildekai) (community)
- **Categories:** Developer tools, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 checked domains

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bulk DMARC, SPF & DKIM Checker

Give the Actor a list of domains. For each domain it checks the email security setup in public DNS and returns a grade (A–F), every problem with a fix, and a one-sentence sales hook.

Made for MSPs and security consultants who prospect with "your domain can be spoofed", for cold-email and deliverability agencies, and for IT teams that audit their own domains. Thousands of domains per run (about 100 seconds per 1,000 domains at the default 2 GB memory); no email is sent.

### What is checked

| Check | Details |
|---|---|
| **SPF** | Record count, syntax, `all` rule (`+all`, `?all`, missing), DNS lookup count against the limit of 10 (includes are followed), void lookups (limit 2), includes that do not exist, `ptr`, sending services (Google Workspace, Microsoft 365, Mailchimp, SendGrid and about 40 more) |
| **DMARC** | Record count, syntax, policy for the domain, subdomains and non-existent subdomains, testing mode (`t=y`) and `pct`, alignment, report providers (dmarcian, Valimail, EasyDMARC, Red Sift and others), report authorization for external report addresses (RFC 9990), tags removed by the new DMARC standard (RFC 9989). A subdomain uses the policy of its organization when it has no own record |
| **DKIM** | Keys at about 40 common selectors plus your own; key type, RSA key size, testing flag, revoked and broken keys |
| **MX** | Mail provider, security gateway (Mimecast, Proofpoint, Barracuda, …), own mail server, null MX, MX hosts that do not resolve |
| **MTA-STS and TLS-RPT** | Record, policy file (one HTTPS request), mode, `max_age`, and whether the policy lists every MX host. A mismatch in `enforce` mode stops mail from senders that use MTA-STS |
| **BIMI** | Record, logo, certificate; BIMI without an enforced DMARC policy |
| **DNSSEC** | The domain is signed |
| **Blocklists** | Own mail server IPs and single IPs in SPF on SpamCop, PSBL, UCEPROTECT level 1 and Mailspike. Shared provider IPs (Google, Microsoft, …) are not checked: their listing says nothing about your domain |
| **Lookalike domains** (optional) | About 100–300 typo, homoglyph and suffix variants (`paypa1.com`, `pyapal.com`, `paypal.net`). Reported: registered variants, which can receive email, and whether they share name servers or own mail servers with your domain |

A domain without MX records and without senders in SPF is checked as a **domain that sends no mail**: it needs `v=spf1 -all` and `p=reject` so nobody can spoof it. DKIM, MTA-STS and DMARC reports are not required for it.

### Ready-made answers

| Field | What it answers |
|---|---|
| `spoofing` | **Can somebody send email as this domain?** `spoofable` yes/no, `status` (`protected` = DMARC reject, `spam_folder` = quarantine, `not_protected`, `unknown`), the `reasons`, and whether subdomains are protected. Only an enforced DMARC policy stops forged From addresses; SPF and DKIM alone do not |
| `bulkSender` | **Gmail, Yahoo and Microsoft bulk sender rules** (rejected with error 550 since May 2026): `ready`, `not_ready`, `unknown` or `not_applicable`, with `missing` and `unverified` items. Checked from DNS: valid SPF, a DKIM key, a DMARC record (at least `p=none`), reverse DNS of the servers that SPF allows. Not visible in DNS: one-click unsubscribe, the spam complaint rate, alignment of each sending service, TLS of sent mail |
| `suggestedRecords` | **Records to paste into DNS**: a repaired SPF record (merged records, `ip4:` added to bare IPs, `ptr` and dead includes removed, `~all` added), the next DMARC step (none → quarantine → reject, removed tags dropped, reports added), `v=spf1 -all` + `p=reject` + null MX for a domain without mail, and a TLS-RPT record. No suggestion when the fix needs knowledge that DNS does not have (unknown terms, more than 10 lookups) |
| `services` | **Services the DNS shows**: mail provider, sending services (SPF), DKIM signers, DMARC report service and about 60 TXT verification records (HubSpot, Atlassian, DocuSign, Zoom, Stripe, OpenAI, …). No extra query |

Every suggested record has `safeToPublish`. **true**: the record cannot stop legitimate mail (it repairs a record that receivers already reject as broken, only adds monitoring or reports, or locks a domain that sends no mail). **false**: review first, because some legitimate mail can fail after it (DMARC quarantine/reject step, SPF built from the mail provider, merged SPF records, removed `+all` or `ptr`, a report address you must fill in).

Report addresses in `suggestedRecords` are kept only when they belong to a report service or a role mailbox (`dmarc@`, `postmaster@`, …); other addresses become `<your-report-address>`.

### Grade

Every finding has a severity. Score = 100 minus 40 per critical, 20 per high, 10 per medium and 3 per low finding. Grade: A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, F below 40. Info findings (for example "BIMI could show your logo") do not change the score.

### Input

```json
{
  "domains": ["acme.com", "https://www.example.org/contact", "info@shop.example.net"],
  "checkLookalikes": false
}
```

- `domains`: up to 10,000 domains, website URLs or email addresses. Only the domain is used; an email address is never written to the output. `www.` is removed; other subdomains are checked as given.
- `sourceDatasetId` and `sourceField`: read domains from another Actor's dataset, for example the `website` field of a Google Maps export.
- `checkDkim`, `extraDkimSelectors`, `checkMtaSts`, `checkBlocklists`, `checkLookalikes`, `maxConcurrency`.

### Sample output

Shortened result for a fictional domain:

```json
{
  "domain": "acme-hotel.com",
  "status": "checked",
  "grade": "C",
  "score": 71,
  "summary": "Grade C: DMARC policy is none (monitor only); No DKIM key found among 42 common selectors.",
  "salesHook": "DMARC of acme-hotel.com only monitors (p=none): forged email still reaches inboxes.",
  "spoofing": {"spoofable": true, "status": "not_protected", "reasons": ["DMARC policy is none (monitor only)"], "subdomainsProtected": false},
  "bulkSender": {"status": "unknown", "missing": [], "unverified": ["DKIM (no key among 42 common selectors)"]},
  "services": ["Microsoft 365", "Google site verification"],
  "suggestedRecords": [
    {"type": "TXT", "name": "_dmarc.acme-hotel.com", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@acme-hotel.com;",
     "safeToPublish": false,
     "reason": "Next step p=quarantine, when the reports show that all your senders pass; added reports to dmarc-reports@acme-hotel.com (create the mailbox)."}
  ],
  "mail": {"receivesMail": true, "mxHosts": ["acme-hotel-com.mail.protection.outlook.com"], "providers": ["Microsoft 365"]},
  "spf": {"record": "v=spf1 include:spf.protection.outlook.com -all", "valid": true, "allQualifier": "fail", "lookupCount": 1},
  "dmarc": {"policy": "none", "effectivePolicy": "none", "enforced": false, "reportDomains": []},
  "dkim": {"selectorsChecked": 42, "found": false, "keys": []},
  "findings": [
    {"id": "dmarc_policy_none", "severity": "medium", "title": "DMARC policy is none (monitor only)",
     "fix": "Read the reports, fix the senders, then move to p=quarantine and p=reject."},
    {"id": "dkim_not_found", "severity": "medium", "title": "No DKIM key found among 42 common selectors",
     "fix": "Turn on DKIM signing at every sending service and publish the keys."}
  ]
}
```

Use the **Overview** view or the `overviewCsv` output link for a spreadsheet: one row per domain with grade, score, summary, sales hook, spoofable, bulk sender status, services, mail provider, SPF, DMARC, DKIM, MTA-STS, BIMI and DNSSEC.

Each item has a `status`: `checked`, `not_found` (the domain does not exist), `error` (DNS failed) or `invalid` (the input is not a domain). `incompleteChecks` lists checks whose DNS lookups failed; a failed lookup is never reported as "missing".

### Pricing

Pay per event:

- **$0.005 per checked domain** (`domain-checked`).
- **$0.025 per lookalike scan** (`lookalike-scan`): an extra event per checked domain, only when `checkLookalikes` is on.
- Domains that do not exist, DNS errors and invalid inputs are free.

Set a maximum charge for the run; the Actor stops cleanly when the next domain would go over it.

### Limits

- **DKIM:** DNS cannot list DKIM selectors. "Not found among 42 common selectors" does not prove that the domain has no DKIM. Add selectors you know (from the `s=` tag of an email header) in `extraDkimSelectors`.
- **No SMTP:** the Actor sends no email and does not connect to mail servers (port 25 is closed on the platform). STARTTLS support is not checked.
- **Lookalikes:** the owner of a variant is not known (no WHOIS). Same name servers or same own mail servers are a hint that the variant belongs to you, not proof. Large brands register many variants themselves.
- **Blocklists:** only the free lists above. Before each run the Actor tests every list with its test address; a list that does not answer correctly from the platform is not used (see `blocklistsUsed` in the run summary).
- Subdomains of hosting platforms (for example `shop.webflow.io`) inherit DMARC from the platform domain.

### Personal data

The Actor reads public DNS records only. DMARC and TLS-RPT report addresses can contain a person's name, so only their domain is returned. No WHOIS data, no names, no email addresses.

### Run summary

The key-value store record `RUN_SUMMARY` has counts by status and grade, duplicates, the blocklists used, DNS query and error counts, and the run time.

# Actor input Schema

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

Up to 10,000 domains, website URLs or email addresses (only the domain is used). www. is removed; other subdomains are checked as given.

## `sourceDatasetId` (type: `string`):

A dataset of another Actor, for example a Google Maps export. The Actor reads one domain or website from each item.

## `sourceField` (type: `string`):

Name of the dataset field that holds the domain, website or email address.

## `checkDkim` (type: `boolean`):

Probe about 40 common DKIM selectors. DNS cannot list selectors, so a key at another selector is not found.

## `extraDkimSelectors` (type: `array`):

Selectors you know, for example from an email header (s=...).

## `checkMtaSts` (type: `boolean`):

Reads the MTA-STS policy file with one HTTPS request when the domain publishes MTA-STS.

## `checkBlocklists` (type: `boolean`):

Own mail server IPs and single IPs in SPF on four free IP blocklists. Shared provider IPs (Google, Microsoft and others) are not checked.

## `checkLookalikes` (type: `boolean`):

Checks about 300 typo, homoglyph and suffix variants of each domain and reports the registered ones and those that can receive email. Charged as an extra event per domain.

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

How many domains are checked at the same time.

## Actor input object example

```json
{
  "domains": [
    "apify.com"
  ],
  "sourceField": "website",
  "checkDkim": true,
  "extraDkimSelectors": [],
  "checkMtaSts": true,
  "checkBlocklists": true,
  "checkLookalikes": false,
  "maxConcurrency": 50
}
```

# Actor output Schema

## `domains` (type: `string`):

One item per domain: grade, findings with fixes, SPF, DMARC, DKIM, MTA-STS, BIMI, DNSSEC, blocklists and lookalikes.

## `overviewCsv` (type: `string`):

One row per domain with the main columns.

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

Counts by status and grade, DNS queries and errors.

# 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": [
        "apify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tildekai/bulk-dmarc-spf-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": ["apify.com"] }

# Run the Actor and wait for it to finish
run = client.actor("tildekai/bulk-dmarc-spf-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": [
    "apify.com"
  ]
}' |
apify call tildekai/bulk-dmarc-spf-dkim-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tildekai/bulk-dmarc-spf-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/4qIV9cAz1TyLiPgOP/builds/ZT3cxVyW9M8Wl1uGZ/openapi.json
