# Bulk DNS Lookup: MX, SPF & DMARC Checker, Email Provider (`ataism/dns-records`) Actor

Bulk DNS lookup: MX, SPF and DMARC checker, email provider (Google Workspace, Microsoft 365), A, TXT, NS, CAA. No WHOIS. $2 per 1,000 domains.

- **URL**: https://apify.com/ataism/dns-records.md
- **Developed by:** [Kaizoku Data](https://apify.com/ataism) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 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.

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 DNS Lookup: MX, SPF & DMARC Checker, Email Provider

Is the prospect on Google Workspace or Microsoft 365? Are their SPF and DMARC records set up? Does the domain even exist? Paste a list of domains (or URLs, or email addresses) and get one clean row per domain: **A, AAAA, MX, TXT, NS, CNAME and CAA records, the email provider, and an SPF and DMARC check.**

**Price: $2.00 per 1,000 domains ($0.002 per domain), everything included.** Invalid entries, duplicates and domains the resolvers couldn't answer are free.

- **MX record checker and email provider detection:** Google Workspace, Microsoft 365, Zoho, Proton and 20+ other providers, with a best-effort guess when the MX shows only a filtering gateway (Proofpoint, Mimecast, Barracuda).
- **SPF checker:** `-all` or `~all`, lookup-count estimate, several SPF records, unknown terms.
- **DMARC checker:** policy (`none`, `quarantine`, `reject`), subdomain policy, `pct`, and the domains that receive the reports.
- **Dead-domain cleaning:** NXDOMAIN rows come back as `exists: false`, so you can drop them before outreach.
- **Quick start:** press *Start*. The prefilled input checks `example.com` and `github.com` in a few seconds for $0.004. No API keys, no proxies, no browser: the Actor asks public **DNS-over-HTTPS** resolvers.

**DNS only, no WHOIS.** It does not return registrar, owner, creation or expiry dates. If you need those, use a WHOIS/RDAP Actor.

### What you can do with it

- **Lead qualification.** Is the prospect on Google Workspace or Microsoft 365? Does the domain even exist? Filter a list before outreach.
- **Email deliverability audits.** Check SPF (`-all` or `~all`, lookup count, several records) and DMARC (`none`, `quarantine`, `reject`) across hundreds of client or vendor domains.
- **Security and brand monitoring.** Spot dangling CNAMEs, changed name servers, missing CAA, or lookalike domains that resolve.
- **Migration checks.** Confirm that A, MX and NS records point where you expect after a DNS move.
- **Dead-domain cleaning.** NXDOMAIN rows are reported as `exists: false`, so you can drop unregistered domains from a list.

### Input

| Field | What it does |
|---|---|
| **Domains** | A list. Bare domains, URLs (`https://example.com/page`) and email addresses (the domain part is used) all work. Names are lower-cased, converted to punycode (`bücher.ch` becomes `xn--bcher-kva.ch`) and de-duplicated. Entries that are not valid domain names are skipped and not charged. |
| **Record types** | Any of `A`, `AAAA`, `MX`, `TXT`, `NS`, `CNAME`, `CAA`. Default: all except CAA. |
| **Detect email provider** | On by default. Works out the mail provider from the MX records. |
| **Check SPF and DMARC** | On by default. Parses the SPF record and the `_dmarc` policy. |
| **Strip "www."** | Off by default. Turns `www.example.com` into `example.com`. |
| **Max domains** | Look up at most this many domains. |

Names are looked up exactly as given: a subdomain such as `mail.example.com` is queried as itself (it will usually have no NS records of its own).

#### Example inputs

**1. Lead qualification: who hosts the prospect's email?** Paste email addresses or website URLs straight from your CRM export; only the domain part is used.

```json
{
  "domains": ["sales@acme.io", "https://www.stripe.com/pricing", "github.com"],
  "recordTypes": ["MX"],
  "detectEmailProvider": true,
  "checkSpfDmarc": true,
  "stripWww": true
}
```

**2. Email deliverability audit of client or vendor domains** (SPF and DMARC are checked whatever record types you pick; TXT shows the raw records):

```json
{
  "domains": ["example.com", "github.com", "bbc.co.uk"],
  "recordTypes": ["MX", "TXT"],
  "detectEmailProvider": true,
  "checkSpfDmarc": true
}
```

**3. DNS migration or security check** (addresses, name servers, aliases and CAA only; email checks off):

```json
{
  "domains": ["example.com", "www.example.com", "github.com"],
  "recordTypes": ["A", "AAAA", "NS", "CNAME", "CAA"],
  "detectEmailProvider": false,
  "checkSpfDmarc": false,
  "maxDomains": 5000
}
```

### Output

One item per domain (trimmed example; real runs include all fields below):

```json
{
  "domain": "github.com",
  "exists": true,
  "records": {
    "A": ["20.205.243.166"],
    "AAAA": [],
    "MX": [{ "priority": 0, "exchange": "github-com.mail.protection.outlook.com" }],
    "TXT": ["v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com … ~all"],
    "NS": ["dns1.p08.nsone.net", "ns-1283.awsdns-32.org"],
    "CNAME": [],
    "CAA": []
  },
  "ttl": { "A": 60, "AAAA": null, "MX": 300, "TXT": 300, "NS": 900, "CNAME": null, "CAA": null },
  "emailProvider": "Microsoft 365",
  "emailProviders": ["Microsoft 365"],
  "spf": {
    "present": true,
    "record": "v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com … ~all",
    "includes": ["spf.protection.outlook.com", "mail.zendesk.com", "sendgrid.net"],
    "mechanisms": ["ip4:192.30.252.0/22", "include:spf.protection.outlook.com", "~all"],
    "redirect": null,
    "all": "~all",
    "lookupCountEstimate": 8,
    "valid": true,
    "issues": []
  },
  "dmarc": {
    "present": true,
    "record": "v=DMARC1; p=quarantine; sp=reject; pct=100; rua=mailto:[email removed]; ruf=mailto:[email removed]; fo=1",
    "policy": "quarantine",
    "subdomainPolicy": "reject",
    "pct": 100,
    "ruaDomains": ["github.com"],
    "rufDomains": ["github.com"],
    "valid": true
  },
  "recordTypes": ["A", "AAAA", "MX", "TXT", "NS", "CNAME", "CAA"],
  "resolver": "cloudflare",
  "checkedAt": "2026-10-18T06:00:00.000Z",
  "source": "Public DNS over HTTPS (Cloudflare 1.1.1.1, AdGuard DNS as fallback)",
  "error": null
}
```

A domain that does not exist comes back as `"exists": false` with empty record lists. Export as JSON, CSV, Excel or XML, or read it through the Apify API. The dataset has three views: *Domains*, *Email security* and *All records*.

Notes on the fields:

- **`records`** always has all seven keys. A type you did not request is an empty array; `recordTypes` tells you what was requested.
- **`ttl`** is the lowest TTL per type as reported by the resolver. A cached answer shows the time left, so it can be lower than the zone's configured TTL.
- **`emailProvider`** is the provider of the lowest-priority recognised MX host. It is `other` when MX records exist but none is recognised, and `none` when there is no MX or a null MX. Recognised: Google Workspace, Microsoft 365 (including `*.mx.microsoft` and `protection.outlook.us` hosts), Zoho Mail, Proton Mail, Fastmail, iCloud Mail, Yahoo Mail, Amazon SES / WorkMail, Mimecast, Proofpoint, Barracuda, Cloudflare Email Routing, GoDaddy, OVH, IONOS, Yandex, Mail.ru, Tencent / QQ, Namecheap Private Email and Email Forwarding, Hostinger, Gandi, Rackspace, Cisco Secure Email, Mailgun, SendGrid, Zimbra. `emailProviders` lists every recognised provider from the MX set. **Gateways:** when the MX shows only a filtering gateway (Proofpoint, Mimecast, Barracuda and similar), the real mailbox provider is not visible in MX. In that case, if **Check SPF and DMARC** is on, we add a best-effort guess from the SPF includes (for example `spf.protection.outlook.com` suggests Microsoft 365, `_spf.google.com` suggests Google Workspace) to `emailProviders`. It is an inference, not proof: `emailProvider` keeps naming what the MX says.
- **`spf.lookupCountEstimate`** counts the lookup terms in the domain's own record (`include`, `a`, `mx`, `ptr`, `exists`, `redirect`). It does not follow includes, so the true total can be higher. `spf.valid` is false when there is no record, there are several SPF records, a term is unknown, or more than 10 lookups are counted.
- **`exists`** is `false` only when the resolver answers NXDOMAIN and nothing else shows the name alive. A dangling alias (a CNAME whose target is gone) counts as existing. If the answers contradict each other (NXDOMAIN for one type, data for another), `exists` is `null` with an `error` and the row is not charged.
- **Records belong to the name you asked for.** An alias has no NS, MX or TXT of its own, so a CNAME'd subdomain shows empty NS/MX/TXT rather than its target's records.
- **CNAME chains**: like any resolver, an `A` lookup of a name that is an alias returns the addresses at the end of the chain.

**Fill rates** in our recorded sample of 99 existing domains (2026-10-17): `NS` 100%, `A` 98%, `TXT` 97%, `MX` 95%, SPF present 96%, DMARC present 95%, `AAAA` 43%, `CAA` 48%, `CNAME` 1%. Low numbers are records most domains simply don't have.

### Pricing

Pay per event:

- **$0.002 per domain checked.**
- Apify's standard tiny start fee per run.

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

You are charged once per input domain that got a **definitive answer**, including "this domain does not exist" (NXDOMAIN). You are **not** charged for entries that are not valid domains, for duplicates, or for domains the resolvers could not answer (those rows carry an `error` message so you can retry them). You can cap any run with Apify's *Max total charge* setting; the Actor stops cleanly when it reaches it.

### Use it via API, n8n or Make

Get an API token in Apify Console → *Settings* → *API & Integrations*, and keep it in an environment variable (`APIFY_TOKEN` below). Never paste it into shared code.

**cURL** (starts a run, waits for it and returns the rows as JSON; this synchronous endpoint waits up to 5 minutes, which fits a few hundred domains; for bigger lists use the clients below):

```bash
curl -X POST "https://api.apify.com/v2/acts/ataism~dns-records/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["github.com", "stripe.com"], "recordTypes": ["MX", "TXT"]}'
```

**JavaScript** (`npm install apify-client`):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('ataism/dns-records').call({
    domains: ['github.com', 'stripe.com'],
    recordTypes: ['MX', 'TXT'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const row of items) console.log(row.domain, row.emailProvider, row.dmarc?.policy);
```

**Python** (`pip install "apify-client>=3"`):

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("ataism/dns-records").call(run_input={
    "domains": ["github.com", "stripe.com"],
    "recordTypes": ["MX", "TXT"],
})
for row in client.dataset(run.default_dataset_id).iterate_items():
    print(row["domain"], row["emailProvider"], (row.get("dmarc") or {}).get("policy"))
```

(With apify-client versions before 3.0, `call()` returns a dict: use `run["defaultDatasetId"]`.)

**n8n** (enrich a sheet of domains):

1. Read the domain column with a *Google Sheets* node and combine it into one list (for example with an *Aggregate* node).
2. Add the **Apify** node, operation **Run an Actor and get dataset**, Actor `ataism/dns-records`, input `{"domains": <your list>}`.
3. Write `emailProvider`, `spf.valid`, `dmarc.policy` and `exists` back with a *Google Sheets* update node, matching on `domain`.

**Make:**

1. Add the **Apify** module **Run an Actor**, choose `ataism/dns-records`, map your domain list into the input JSON and set *Run synchronously* to *Yes*.
2. Add the **Apify** module **Get Dataset Items** and map the run's default dataset ID.
3. Add Google Sheets (*Bulk Add Rows* or *Update a Row*) or your CRM and map the fields.

### Privacy

DNS records are public information about organisations. This Actor:

- does **no WHOIS or RDAP lookups**, so it never returns registrant names, emails or phone numbers;
- **does not output the SOA record**, whose contact field is a person's or team's mailbox;
- **replaces any email address** found inside TXT or CAA values with `[email removed]`;
- reduces DMARC `rua`/`ruf` report addresses to their **receiving domains** only (`ruaDomains`, `rufDomains`); the raw `mailto:` addresses are never output.

The resolvers you query through (Cloudflare, with AdGuard as backup) see the domain names you look up, as with any DNS query.

### Reliability

- Uses two public DNS-over-HTTPS JSON resolvers: **Cloudflare 1.1.1.1** first, **AdGuard DNS** (non-filtering) as fallback. An NXDOMAIN answer is final and is not second-guessed.
- At most 5 queries in flight, a timeout per query, and retries with exponential backoff on timeouts, HTTP 429 and 5xx (honouring `Retry-After`). It never tries to bypass a limit.
- Domains that fail on both resolvers are returned with an `error` and no charge.
- A daily self-test queries 10 stable domains (for example example.com, github.com, bbc.co.uk) plus a made-up name, checks fixed expectations (such as "example.com has A and NS records") rather than exact IPs, cross-checks NS records between the two resolvers, and compares field fill rates and the resolver response format with a recorded baseline. Breakages are caught within a day.

### FAQ

**Is it allowed?** Yes. DNS records are public by design, and we query public resolvers through their documented DNS-over-HTTPS JSON interfaces, at a low and polite rate. What the providers publish (checked 2026-10-18):

- **Cloudflare 1.1.1.1** (primary). Its DNS JSON API page ([developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/)) documents the endpoint for general use and states no rate limit or acceptable-use clause; it only warns that the JSON format has no RFC, so "behavior might be different between providers". Its privacy page ([developers.cloudflare.com/1.1.1.1/privacy/public-dns-resolver](https://developers.cloudflare.com/1.1.1.1/privacy/public-dns-resolver/)) says the resolver "is governed by our Privacy Policy", that "all Public Resolver Logs are deleted within 25 hours", and that Cloudflare "does not block or filter any content through the 1.1.1.1 Public DNS Resolver". No rate limits are mentioned there either.
- **AdGuard DNS** (fallback, only used when Cloudflare fails). Its public DNS page ([adguard-dns.io/en/public-dns.html](https://adguard-dns.io/en/public-dns.html)) lists the non-filtering server and says nothing about the JSON `/resolve` API, rate limits or acceptable use; its privacy page ([adguard-dns.io/en/privacy.html](https://adguard-dns.io/en/privacy.html)) says "We do not process any of your Personal Data when you connect to and use public AdGuard DNS." Because its terms are silent on automated use, it is a low-volume backup and the self-test's 10 cross-check queries only.
- **Not used:** Google Public DNS (excluded by project policy) and **Quad9**: its JSON endpoint on port 5053 did not answer from our test machine (connection timeout) and its current docs list only the standard RFC 8484 endpoint (`https://dns.quad9.net/dns-query`), which does not return JSON.

We keep to at most 5 queries in flight, back off on errors and never try to evade a limit. If a provider publishes limits or objects, we will change the Actor.

**Do you provide WHOIS data (registrar, owner, expiry)?** No, on purpose. See *Privacy*.

**Why is `exists` false for a domain I know?** The resolver reported NXDOMAIN for that exact name. Check for typos, and note that subdomains are looked up as given.

**Why does a subdomain have no NS records?** NS records are normally published at the zone apex (`example.com`), not on every subdomain.

**Does it do reverse DNS, DNSSEC validation or DKIM?** Not in this version. DKIM needs the selector, which is not discoverable from DNS.

**Something's wrong?** Open an issue on the Actor's Issues tab. We aim to reply within 2 working days. Not affiliated with Cloudflare, AdGuard, or any email provider named above.

# Actor input Schema

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

Domains to look up. Bare domains, URLs and email addresses are all accepted (the domain part is used). Names are lower-cased, converted to punycode and de-duplicated.

## `recordTypes` (type: `array`):

Which DNS record types to return. SPF and DMARC are analysed separately (see below) and do not depend on this setting.

## `detectEmailProvider` (type: `boolean`):

Work out who hosts the domain's email (Google Workspace, Microsoft 365, Zoho, Proton, …) from its MX records.

## `checkSpfDmarc` (type: `boolean`):

Parse the SPF record and the \_dmarc policy. DMARC report addresses are reduced to their domains.

## `stripWww` (type: `boolean`):

Turn www.example.com into example.com before looking it up. Off by default: the name is looked up exactly as given.

## `maxDomains` (type: `integer`):

Look up at most this many domains (after de-duplication). Leave empty for no limit.

## `mode` (type: `string`):

Internal: "selfTest" runs the maintainer health check and saves no domains.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "github.com"
  ],
  "recordTypes": [
    "A",
    "AAAA",
    "MX",
    "TXT",
    "NS",
    "CNAME"
  ],
  "detectEmailProvider": true,
  "checkSpfDmarc": true,
  "stripWww": false,
  "mode": "lookup"
}
```

# Actor output Schema

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

DNS records per domain, one item per row (JSON, CSV, Excel or XML).

## `runSummary` (type: `string`):

Counts, skipped inputs and errors for this run.

# 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": [
        "example.com",
        "github.com"
    ],
    "recordTypes": [
        "A",
        "AAAA",
        "MX",
        "TXT",
        "NS",
        "CNAME"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ataism/dns-records").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": [
        "example.com",
        "github.com",
    ],
    "recordTypes": [
        "A",
        "AAAA",
        "MX",
        "TXT",
        "NS",
        "CNAME",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("ataism/dns-records").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": [
    "example.com",
    "github.com"
  ],
  "recordTypes": [
    "A",
    "AAAA",
    "MX",
    "TXT",
    "NS",
    "CNAME"
  ]
}' |
apify call ataism/dns-records --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ataism/dns-records"
        }
    }
}
```

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/Un3CubLufAuZydhS9/builds/3RChm3mZVwWcFmSIF/openapi.json
