# Bulk DMARC Record Checker (`automation-lab/bulk-dmarc-record-checker`) Actor

Check DMARC records in bulk and export raw records, parsed policies, reporting destinations, alignment settings, and validation findings.

- **URL**: https://apify.com/automation-lab/bulk-dmarc-record-checker.md
- **Developed by:** [Stas Persiianenko](https://apify.com/automation-lab) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.96 / 1,000 domain extracteds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bulk DMARC Record Checker

Bulk DMARC Record Checker is a **DMARC record checker** for domain portfolios. Give it up to 5,000 domains or website URLs and receive one normalized audit row per unique domain with the raw TXT record, parsed policy tags, reporting destinations, alignment settings, validation findings, and explicit missing or DNS-error states.

It uses the public DNS record at `_dmarc.<domain>`. It does not crawl websites, require login cookies, or hide missing records by dropping rows.

### What does Bulk DMARC Record Checker do?

The Actor performs these steps for every unique normalized domain:

1. Converts a domain or URL to a lowercase ASCII hostname.
2. Resolves TXT values at `_dmarc.<domain>`.
3. Joins multi-string DNS TXT chunks correctly.
4. Selects values beginning with the `DMARC1` version marker.
5. Distinguishes one record, no record, multiple records, and DNS errors.
6. Parses policy, reporting, percentage, alignment, failure, interval, and format tags.
7. Emits structured findings for malformed, duplicate, invalid, unusual, and unknown tags.
8. Saves one typed dataset item even when the record is missing or the resolver fails.

This output supports one-time checks and scheduled email-security posture audits. A scheduled Apify Task can run the same domain list repeatedly; each run's `checkedAt` timestamp and dataset provide a snapshot for downstream comparison.

### Who is it for?

- **Email-security teams** auditing whether managed domains publish enforceable policies.
- **IT and Google Workspace administrators** checking policy, reporting, SPF alignment, and DKIM alignment.
- **Managed service providers** reviewing many customer domains in one repeatable run.
- **Security consultants** producing source-backed domain posture inventories.
- **Data and compliance teams** exporting normalized findings to a spreadsheet, database, or SIEM.
- **Developers** integrating DMARC record checks into onboarding or domain-management workflows.

Use [Bulk DNS Records Lookup](https://apify.com/automation-lab/bulk-dns-lookup) when you need A, AAAA, MX, NS, TXT, CNAME, and SOA records rather than DMARC-specific parsing. Use [MX Record Checker](https://apify.com/automation-lab/mx-record-checker) when the job is specifically mail-exchanger discovery.

### Why use this Actor?

A raw DNS lookup gives you strings. This Actor converts those strings into audit-ready fields while preserving the source record.

- **Bulk input:** up to 5,000 supplied domains per run.
- **Explicit coverage:** missing and resolver-error states are rows, not silent gaps.
- **Audit evidence:** every row includes the query name, raw record, timestamp, and duration.
- **Policy parsing:** `p`, `sp`, `pct`, `adkim`, `aspf`, `rua`, `ruf`, `fo`, `ri`, and `rf` become typed fields.
- **Validation findings:** stable codes make filtering and downstream alerting practical.
- **International domains:** Unicode hostnames normalize through IDNA/ASCII conversion.
- **URL input:** `https://www.domain.tld/path` is checked as `www.domain.tld`.
- **Custom resolvers:** optional DNS server IPs support resolver-specific audits.
- **No browser or proxy:** the Actor uses the DNS protocol directly.

### What DMARC data is returned?

| Field | Meaning |
| --- | --- |
| `input` | Original domain or URL supplied by the user |
| `domain` | Normalized, deduplicated ASCII hostname |
| `queryName` | DNS name queried, such as `_dmarc.domain.tld` |
| `status` | `valid`, `invalid`, `missing`, `multiple`, or `error` |
| `hasRecord` | Whether a DMARC-version TXT record was found |
| `isValid` | Whether exactly one record passed implemented checks |
| `rawRecord` | Single source record, or `null` when unavailable/ambiguous |
| `rawRecords` | All detected DMARC-version records |
| `parsed.policy` | Organizational policy: `none`, `quarantine`, or `reject` |
| `parsed.subdomainPolicy` | Optional subdomain policy from `sp` |
| `parsed.percentage` | Effective `pct`; defaults to 100 when omitted |
| `parsed.aggregateReportUris` | Parsed `rua` destinations, including address and size limit |
| `parsed.forensicReportUris` | Parsed `ruf` destinations |
| `parsed.dkimAlignment` | `adkim`; defaults to relaxed (`r`) |
| `parsed.spfAlignment` | `aspf`; defaults to relaxed (`r`) |
| `parsed.failureOptions` | Parsed `fo` values |
| `parsed.reportIntervalSeconds` | `ri`; defaults to 86,400 seconds |
| `parsed.reportFormat` | `rf`; defaults to `afrf` |
| `parsed.tags` | Every parsed tag and its original value |
| `parsed.unknownTags` | Unrecognized tags preserved for forward compatibility |
| `findings` | Structured validation errors, warnings, and information |
| `errorCode`, `errorMessage` | Resolver details when status is `error` |
| `checkedAt` | ISO 8601 result timestamp |
| `durationMs` | Lookup and parsing duration for the domain |

### How to run your first DMARC record check

1. Open the Actor in Apify Console.
2. Enter one or more domains in **Domains**.
3. Keep the default concurrency and timeout for a first run.
4. Click **Start**.
5. Open the **Dataset** tab and select the DMARC audit overview.
6. Filter `status` or `isValid`, or export the complete dataset as JSON, CSV, Excel, XML, or RSS.

Example input:

```json
{
  "domains": [
    "google.com",
    "yahoo.com",
    "https://github.com/about",
    "neverssl.com"
  ],
  "concurrency": 20,
  "timeoutMs": 5000
}
```

Duplicate normalized domains are checked once. In this example, a second `github.com` input would not create a duplicate charge or row.

### Input parameters

#### `domains`

Required array of 1–5,000 domains or website URLs. Each entry must resolve to a syntactically valid public hostname containing at least one dot. IP addresses, empty values, and malformed labels fail input validation.

The hostname is used exactly as normalized. Supplying `www.domain.tld` checks `_dmarc.www.domain.tld`; it does not automatically change the request to the organizational domain.

#### `concurrency`

Optional integer from 1 to 100. The default is 20. Lower it when your custom recursive resolver applies strict rate limits.

#### `timeoutMs`

Optional timeout from 500 to 30,000 milliseconds for each resolver attempt. The default is 5,000.

#### `dnsServers`

Optional list of IPv4 or IPv6 recursive DNS server addresses. When set, the Actor preserves this resolver choice and does not substitute public fallback resolvers.

### Example output

A live local check of `google.com` produced a row with this shape (timestamp and duration shortened for readability):

```json
{
  "input": "google.com",
  "domain": "google.com",
  "queryName": "_dmarc.google.com",
  "status": "valid",
  "hasRecord": true,
  "isValid": true,
  "rawRecord": "v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com",
  "rawRecords": [
    "v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com"
  ],
  "parsed": {
    "version": "DMARC1",
    "policy": "reject",
    "subdomainPolicy": null,
    "percentage": 100,
    "aggregateReportUris": [
      {
        "uri": "mailto:mailauth-reports@google.com",
        "scheme": "mailto",
        "address": "mailauth-reports@google.com",
        "sizeLimit": null
      }
    ],
    "forensicReportUris": [],
    "dkimAlignment": "r",
    "spfAlignment": "r",
    "failureOptions": [],
    "reportIntervalSeconds": 86400,
    "reportFormat": ["afrf"],
    "tags": {
      "v": "DMARC1",
      "p": "reject",
      "rua": "mailto:mailauth-reports@google.com"
    },
    "unknownTags": {}
  },
  "findings": [],
  "errorCode": null,
  "errorMessage": null,
  "checkedAt": "2026-08-23T12:00:00.000Z",
  "durationMs": 24
}
```

### Understanding statuses and findings

- `valid`: one DMARC record was found and no error-severity finding was produced.
- `invalid`: one DMARC record was found, but required syntax or a supported tag value is invalid.
- `missing`: the DNS query had no DMARC-version TXT record.
- `multiple`: more than one DMARC-version record was found, so receivers cannot apply one unambiguous policy.
- `error`: DNS resolution failed for a reason other than an authoritative no-record result.

Finding severities are `error`, `warning`, and `info`. Stable codes include `record_missing`, `multiple_records`, `missing_policy`, `invalid_policy`, `invalid_alignment`, `invalid_percentage`, `duplicate_tag`, `unknown_tag`, and `dns_lookup_failed`.

Validation is an operational aid, not a guarantee that every receiver will interpret a policy identically. The Actor does not fetch aggregate reports or verify ownership of reporting mailboxes.

### How much does it cost to check DMARC records?

The Actor uses pay-per-event pricing:

- one `start` event per run;
- one `domain` event per unique domain result, including explicit missing and error rows.

Current pricing is a **$0.001 start fee** plus a per-domain price that decreases by Apify plan:

| Apify plan | Price per unique domain |
| --- | ---: |
| Free | $0.00184 |
| Bronze | $0.0016 |
| Silver | $0.001248 |
| Gold | $0.00096 |
| Platinum | $0.00064 |
| Diamond | $0.000448 |

At the Bronze rate:

| Unique domains | Estimated Actor charge |
| ---: | ---: |
| 1 | $0.0026 |
| 25 | $0.041 |
| 100 | $0.161 |
| 1,000 | $1.601 |

The input limit, deduplication, and event names are part of the pricing contract. DNS records themselves are public; charges pay for the repeatable processing, validation, structured output, and Apify execution.

### Scheduled audit and monitoring workflows

Create an Apify Task with a stable `domains` list, then attach a daily, weekly, or monthly schedule. Each run creates a separate timestamped dataset snapshot.

Useful downstream patterns include:

1. Send each dataset to Google Sheets and filter domains whose `status` is not `valid`.
2. Store snapshots in a warehouse and compare `rawRecord` or `parsed.policy` over time.
3. Trigger a webhook after the run and create alerts for `missing`, `multiple`, or `error` states.
4. Enrich an asset inventory with policy, reporting, and alignment columns.
5. Audit customer onboarding lists before email infrastructure changes.

The Actor reports current DNS answers. It does not retain history or send alerts by itself; Apify schedules, webhooks, integrations, and your downstream system provide those workflow layers.

### Export and integrations

Every result is written to the default Apify dataset. Use Console export or the dataset API to retrieve JSON, JSONL, CSV, Excel, XML, or RSS.

Common integrations:

- Google Sheets for audit review;
- Make or Zapier for status-based workflows;
- Slack or email webhooks for policy changes;
- BigQuery, Snowflake, or PostgreSQL for snapshot comparison;
- SIEM and asset-management systems for posture enrichment;
- Apify API clients for application onboarding checks.

### Run with the Apify API

Set `APIFY_TOKEN` in your environment rather than placing it in source code.

#### cURL

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/automation-lab~bulk-dmarc-record-checker/runs" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domains":["google.com","github.com"],"concurrency":10}'
```

#### JavaScript

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/bulk-dmarc-record-checker').call({
  domains: ['google.com', 'github.com', 'neverssl.com'],
  concurrency: 10,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

#### Python

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/bulk-dmarc-record-checker').call(run_input={
    'domains': ['google.com', 'github.com', 'neverssl.com'],
    'concurrency': 10,
})
items = client.dataset(run['defaultDatasetId']).list_items().items
print(items)
```

### Use Bulk DMARC Record Checker through MCP

Add this Actor to Claude Code:

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=automation-lab/bulk-dmarc-record-checker"
```

#### Claude Desktop setup

Add this server in Claude Desktop's MCP configuration. The same JSON format is also supported by editor clients:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=automation-lab/bulk-dmarc-record-checker"
    }
  }
}
```

#### Cursor setup

Add the JSON server entry in Cursor's MCP settings, then enable the `apify` server for the workspace.

#### VS Code setup

Add the same HTTP MCP server in VS Code's MCP configuration and authorize it with your Apify account when prompted.

Example prompts:

- “Check DMARC for these 30 domains and return only missing or invalid records.”
- “Parse DMARC policy, rua, adkim, and aspf for google.com and github.com.”
- “Run the bulk DMARC checker on my approved asset list and summarize reject versus quarantine policies.”

### Reliability, retries, and DNS behavior

The Actor uses the runtime recursive resolver with two bounded attempts. For transient timeout, refusal, or server-failure codes, it can make one recovery query through independent public recursive resolvers. Authoritative no-record results are not retried.

When `dnsServers` is supplied, the Actor does not replace those servers. This makes split-horizon or resolver-comparison results predictable, but availability depends on the supplied resolver.

DNS answers can differ temporarily because of caching, propagation, DNSSEC failures, and resolver location. The Actor reports the answer observed during the run, not a global propagation consensus.

### Legality

- Maximum 5,000 input values per run.
- Maximum 100 concurrent domain checks.
- Only the exact normalized hostname is queried.
- DMARC organizational-domain fallback behavior is not simulated.
- External report authorization records are not followed or verified.
- The Actor does not create, publish, or modify DNS records.
- The Actor does not receive, parse, or store DMARC aggregate/forensic report files.
- A syntactically valid record can still represent a weak policy such as `p=none`.

DNS records are generally public infrastructure data. Use the Actor only for domains and workflows you are authorized to assess, follow applicable contracts and organizational policies, and protect exported domain inventories when they are sensitive.

### Troubleshooting

#### Why is the status `missing` when a website exists?

A working website does not imply that `_dmarc.<domain>` has a DMARC TXT record. Verify the exact `queryName` field and whether you supplied a hostname such as `www.domain.tld` rather than the intended organizational domain.

#### Why did I receive `error` instead of `missing`?

`missing` is reserved for authoritative no-record responses. `error` means resolution failed because of timeout, refusal, server failure, or another resolver condition. Inspect `errorCode` and `errorMessage`, rerun after the transient issue, or supply a known recursive resolver.

#### Why is a record marked `multiple`?

DMARC expects one policy record. Publishing multiple TXT records beginning with `v=DMARC1` is ambiguous. Consolidate the tags into one record at your DNS provider.

#### Why does a URL check a subdomain?

URLs normalize to their exact hostname. `https://www.domain.tld/page` becomes `www.domain.tld`, not `domain.tld`. Supply the organizational domain explicitly when that is your audit target.

### Frequently asked questions

#### Does this Actor create a DMARC record?

No. It checks and parses records that are already published. It can help identify fields needed in a record, but it never changes DNS.

#### Does it check SPF or DKIM selectors?

No. The product is intentionally DMARC-specific. Use the related DNS or MX Actors for broader DNS inventory, and a dedicated email-authentication workflow when SPF or selector discovery is required.

#### Are missing and DNS-error rows charged?

Yes. They are useful explicit audit results and use the same `domain` event as valid records. Duplicated normalized inputs are processed and charged once.

#### Can I schedule recurring checks?

Yes. Save the input as an Apify Task and attach a schedule. Snapshot comparison and alerting are handled through datasets, integrations, or your downstream system.

#### Does `valid` mean my email is fully protected?

No. It means one record passed the implemented syntax and value checks. Email protection also depends on aligned SPF/DKIM authentication, actual sending configuration, report review, and receiver behavior.

### Related automation-lab Actors

- [Bulk DNS Records Lookup](https://apify.com/automation-lab/bulk-dns-lookup) — normalized A, AAAA, MX, NS, TXT, CNAME, and SOA inventory.
- [MX Record Checker](https://apify.com/automation-lab/mx-record-checker) — mail-exchanger records and provider identification.
- [SSL Certificate Checker](https://apify.com/automation-lab/ssl-certificate-checker) — certificate validity and expiry checks for domain portfolios.

These links are complementary tools from `automation-lab`; third-party Store competitors are not presented as related products.

# Actor input Schema

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

Domains or website URLs to check. URLs are normalized to their hostnames and duplicate domains are checked once.

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

Number of domains checked in parallel. Lower this when using a rate-limited custom DNS resolver.

## `timeoutMs` (type: `integer`):

Timeout for each DNS resolver attempt.

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

Optional IPv4 or IPv6 recursive resolver addresses. Leave empty to use the runtime resolver with bounded public-resolver recovery on transient errors.

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "yahoo.com",
    "github.com"
  ],
  "concurrency": 20,
  "timeoutMs": 5000
}
```

# Actor output Schema

## `overview` (type: `string`):

Default dataset containing one parsed DMARC audit result for each unique normalized domain.

# 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",
        "yahoo.com",
        "github.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/bulk-dmarc-record-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": [
        "google.com",
        "yahoo.com",
        "github.com",
    ] }

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/bulk-dmarc-record-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/qaAHq5e1RNvr8Ou57/builds/64b1ti56hytE7lvmr/openapi.json
