# Domain DNS & Email Policy Monitor - SPF, DMARC, MX

Read public DNS records and SPF, DMARC, MX and supplied DKIM selectors. Compare complete observations over time with per-query errors. No API key or mailbox access required.

Canonical product page: https://mayd-it.com/data-tools/actors/domain-dns-email-monitor/
Public Actor: https://apify.com/maydit/domain-dns-email-monitor

## Example input

```json
{
  "domains": [
    "wordpress.org",
    "apify.com"
  ],
  "comparePrevious": false,
  "maxRunSeconds": 240
}
```

## Full guide

![DNS changes with evidence attached — illustrative workflow](https://api.apify.com/v2/key-value-stores/YGxD9JRpduom8Q5Zd/records/domain-dns-email-monitor-00c9a5be3992.png?signature=eWID7qoYwJ7f7aXwU8xu)

## DNS changes with evidence attached

Document a domain inventory or an email-DNS migration with public record evidence. Each row describes one domain observation, including usable answers, specific findings and any query errors.

[Open this Actor in Console](https://console.apify.com/actors/scigxgTNPBdhOd1I5/input) · [First run](#try-this-actor) · [Results](#result-preview) · [Cost](#usage-and-pricing) · [Full guide](#full-guide)

## Try this Actor

1. **Choose a small scope.** Use the existing two-domain example with `wordpress.org` and `apify.com`, leaving comparison off. Supply DKIM selectors only when you already know them; none are guessed.
2. **Run and review.** Open the dataset and inspect observation `status` and `queryErrors`. Check the run's key-value-store `SUMMARY` for complete, partial and unprocessed domains.
3. **Use the results.** Export JSON for nested DNS records and policy findings. Enable comparison later with a dedicated snapshot namespace and sequential runs.

**Read this before using the data:** This inspects published DNS policies; it does not send email, verify mailbox delivery, fully evaluate SPF chains or apply organizational-domain DMARC fallback. Partial observations do not replace saved baselines, but are still billable.

**Copyable input** — use the JSON editor in Console.

```json
{
  "domains": ["wordpress.org", "apify.com"],
  "comparePrevious": false,
  "maxRunSeconds": 240
}
```

## Result preview

The dataset view highlights these columns:

- **Domain** · `domain`
- **Observation** · `status`
- **SPF record count** · `spf.count`
- **SPF all mechanism** · `spf.allMechanism`
- **DMARC policy** · `dmarc.policy`
- **DMARC policy percentage** · `dmarc.percentage`

Use the original source links and the field definitions below when interpreting the results. CSV works well for flat columns; JSON preserves nested values.

## Usage and pricing

Base result rate on the FREE tier: **$3 per 1,000 domain observations**. The [Pricing tab](https://apify.com/maydit/domain-dns-email-monitor/pricing) shows your plan's result rate and any Actor Start charge. Set a small result limit for the first run; the requested limit is not a guarantee of that many results.

---

## Full guide

Read public DNS records and inspect the published SPF, DMARC and MX information for a list of domains. Optionally query known DKIM selectors and compare each complete observation with its previous snapshot.

Use this to document DNS changes, inspect an email migration, or maintain a structured domain inventory. Each finding includes the public evidence and any query errors. The Actor does not invent a security grade or claim to verify mailbox delivery.

### Quick start

Empty input checks `wordpress.org` and `apify.com`:

```json
{}
```

For your own domain list:

Use the [copyable first-run input](#try-this-actor) above.

No account credentials or API key are required. Queries use the [Cloudflare public DNS-over-HTTPS JSON endpoint](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/). The Actor reads DNS; it does not connect to mailboxes or change domain settings.

### Inputs

| Field | Default | Meaning |
|---|---|---|
| `domains` | `wordpress.org`, `apify.com` | Up to 100 distinct domains. Empty or omitted uses these samples. |
| `dkimSelectors` | none | Up to five known selectors, queried for every domain. |
| `comparePrevious` | false | Opt in to storing and comparing observations. |
| `snapshotName` | `default` | Independent monitor namespace, up to 100 characters. |
| `maxRunSeconds` | 240 | 30–3,600 seconds, also bounded by the platform deadline. |

Use domain names, or origin URLs without paths, ports, credentials or query strings. IP addresses are not accepted. A DKIM selector is the prefix in `selector._domainkey.example.com`, not the entire DNS name. Selectors are never guessed. Each selector must begin with a letter or digit and contain at most 63 letters, digits, dots, underscores or hyphens.

### What each observation contains

One dataset row represents one domain. A row is `complete` when every requested query produced a usable response; otherwise it is `partial`. A legitimate no-data response or NXDOMAIN is a DNS observation, not a service failure.

| Field | Contents |
|---|---|
| `domain`, `observedAt`, `status` | Domain, observation time and complete/partial status. |
| `records` | A, AAAA, MX, NS, TXT and exact-domain DMARC results; additional `DKIM:selector` keys for supplied selectors. |
| `records.*.status` | `answer`, `no_data` or `nxdomain`. A query failure is recorded separately. |
| `records.*.answers` | Answer owner, resolver TTL and record value. TXT chunks are joined. |
| `records.*.authenticatedData` | The resolver's AD flag; this is not a general domain security rating. |
| `spf` | Observed SPF records, count, first all mechanism, simple include/redirect fields and a direct lookup-term count. |
| `dmarc` | Observed exact-domain records, count, `p`, `sp`, and `pct` summary. |
| `mxProviders` | Provider hints derived from recognized MX hostnames, each with the hostname as evidence. Empty means no recognized hint. |
| `dkim` | Supplied selector names, DNS status and returned TXT values. |
| `issues` | Specific informational findings or warnings, each with a code, severity and explanation. |
| `queryErrors` | Resolver, shape, response-size or time-budget errors by query type. |
| `comparisonStatus`, `previousObservedAt`, `changes`, `baselineEligible` | Optional snapshot comparison and whether this observation can update the baseline. |
| `limitations` | The explicit boundaries of the policy checks. |

SPF findings include missing or multiple records, the first permissive `all`, multiple `all` terms and excessive direct lookup terms. DMARC checks summarize policy tags and flag selected invalid/duplicate policy cases. This is a policy inspection, not a full SPF or DMARC evaluator. A sole Null MX record is distinguished from an invalid Null MX mixed with other MX records.

### Compare changes over time

```json
{
  "domains": ["wordpress.org"],
  "comparePrevious": true,
  "snapshotName": "dns-observations"
}
```

The first complete run emits a full observation with `comparisonStatus: "first_observation"` and an empty changes list. Later complete runs compare statuses and sorted record values. TTL countdowns and answer ordering are excluded from the comparison. Unchanged observations are still emitted.

The named key-value store `maydit-dns-email-baselines` holds snapshots by domain, sorted selector list and snapshot namespace. Changing selectors or namespace starts a separate comparison scope. A partial observation has `comparisonStatus: "skipped_partial"` and never replaces previous history. `baselineEligible` describes eligibility; confirmed updates are listed in `SUMMARY.baselinesUpdated` after persistence.

Use a separate `snapshotName` for independent monitors. Avoid overlapping runs with the same name and scope, because key-value storage does not provide an atomic monitor lock. Schedule future runs through Apify only when you want recurring execution; this Actor does not create schedules itself.

### Run summary and failures

`SUMMARY` contains requested/emitted/complete/partial counts, failed domains, query errors, unprocessed domains, deadline status and confirmed baseline updates. If one domain fails completely, usable results from others are retained. If no domain yields any usable query response, the run fails with a diagnostic.

Resolver errors and malformed/truncated responses are not silently converted into missing DNS records. A partial observation can still contain useful DNS evidence; inspect `status` and `queryErrors` before making a decision from it.

### Billing

Launch price: **$3 per 1,000 emitted domain observations** ($0.003 each) on Free/Bronze, plus an Actor Start event of $0.00005. Silver is 20% lower and Gold/Platinum/Diamond 40% lower. The live Pricing tab is authoritative.

**Partial observations are billable**, as are unchanged observations, first snapshots and valid no-data/NXDOMAIN results. A domain whose every query fails produces no dataset row and no result event. Each domain observation is one result; individual DNS answers and DKIM selectors are not separate result events. Consult the published pricing tab for any platform resource charges.

### Limits

- No RDAP/WHOIS, ownership, expiry or registrant lookup is performed.
- No mailbox verification, SMTP probing, email sending or inbox-deliverability test is performed.
- SPF include chains are not expanded. The direct term count is not a complete SPF lookup-budget evaluation.
- DMARC is checked at `_dmarc.<exact-domain>` only. Organizational-domain fallback and policy inheritance are not evaluated.
- DKIM is queried only for supplied selectors. Returned keys do not prove that outbound mail is correctly signed.
- MX provider labels are hostname hints and can miss custom gateways or other providers.
- DNS caches, propagation and geography can affect observations. A public resolver's answer is a point-in-time observation, not proof that every resolver sees the same result.
- `authenticatedData: false` is not, by itself, proof of an insecure domain.

### Development

Run `npm test` for the fixture suite. Use isolated `CRAWLEE_STORAGE_DIR` directories when running locally. The documented examples illustrate supported query patterns; the comparison example explicitly opts into state storage and does not create an external schedule.

## Help with your run

Open the **Issues** tab on this Actor with the run URL, the input field that caused trouble and what you expected to receive. Remove API keys and private information before posting.

[Explore more Mayd-It Actors](https://apify.com/maydit)

## Understand your run

Open **Run report** in the run’s Output tab for saved-record counts, reported source issues and limits, and suggested next steps. Use **CSV export** to take the saved results into a spreadsheet. The existing JSON results and detailed machine-readable summary remain available.

The report reflects this run’s recorded coverage. A successful run or a saved result does not establish that every possible source record was collected.
