# Bulk Domain Lookup: RDAP, DNS, SPF, DMARC & DKIM (`signalscouter/bulk-domain-lookup`) Actor

Check thousands of domains in one run: registrar, creation and expiry dates, status and nameservers from official RDAP (the WHOIS successor), A/MX/NS/TXT records, SPF, DMARC, DKIM selectors and mail provider. No personal data: registrant contacts are removed.

- **URL**: https://apify.com/signalscouter/bulk-domain-lookup.md
- **Developed by:** [Signal Scout](https://apify.com/signalscouter) (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

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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 Domain Lookup: RDAP, DNS, SPF, DMARC & DKIM

Paste a list of domains and get **one row per domain** with:

- **Registration data from RDAP**, the official successor to WHOIS. You get the registrar (name, IANA ID), creation, last-changed, expiry and last-transfer dates, domain age, days until expiry, EPP status codes, the registry's nameservers and DNSSEC. Each domain goes to its registry's own RDAP server, which the Actor finds through the [IANA RDAP bootstrap file](https://data.iana.org/rdap/dns.json).
- **DNS records:** A, AAAA, MX, NS and TXT, plus the DNS host inferred from NS (Cloudflare, Route 53 and so on) and SaaS verification records in TXT (Google, Microsoft 365, Atlassian, OpenAI and more).
- **Email security:** the SPF record, policy and allowed senders, the DMARC record, policy, pct and report domains, and DKIM selectors found by probing common selectors (`google`, `selector1`, `selector2`, `k1`, `k2`, `default`, `s1`, `s2`, `mail`, `dkim` and 6 more). You also get MTA-STS, TLS-RPT, BIMI and an **A–F email-security grade**.
- **Mail provider:** Google Workspace, Microsoft 365, Zoho, Proton and others, read from MX. When MX points at a security gateway such as Proofpoint or Mimecast, the provider behind the gateway is read from SPF instead.

**No personal data.** RDAP sometimes returns registrant, admin, technical or abuse contacts. They are **always removed**: the output only lists which contact *roles* were removed (`redactedContactRoles`), never a name, email, phone or address. Email addresses inside TXT and DMARC records are masked (`rua=mailto:***@acme.com`), so only the domain is kept.

*By signalscouter.* It uses only public RDAP and public DNS: no WHOIS port-43 scraping, no website scraping, no proxies and no API key.

### Who it's for

- **Domain portfolio and brand teams:** expiry dates, registrar locks and nameserver changes across hundreds of domains.
- **Security and deliverability teams:** find domains with missing or weak SPF, DMARC or DKIM.
- **Sales and RevOps:** enrich a lead list with each company's mail provider (Google Workspace or Microsoft 365) and domain age.
- **Fraud and risk teams:** flag young domains or domains with no mail setup.

### Input

```json
{
  "domains": ["stripe.com", "https://www.github.com/about", "jane@example.org"],
  "includeRdap": true,
  "includeEmailSecurity": true,
  "includeTxtRecords": true,
  "onlyNew": false
}
```

| Field | Default | What it does |
|---|---|---|
| `domains` | | Domains, URLs or email addresses, one per line (commas also work). Each is reduced to its registrable domain using the Public Suffix List, so `https://www.shop.example.co.uk/x` becomes `example.co.uk`. Duplicates are merged into one row and one charge, and `inputs` lists every original string. |
| `includeRdap` | true | Registration data from RDAP. |
| `includeEmailSecurity` | true | SPF, DMARC, DKIM, MTA-STS, TLS-RPT, BIMI and the grade. |
| `includeTxtRecords` | true | Outputs the raw apex TXT records. SaaS verifications are detected either way. |
| `keepSubdomains` | false | When on, `mail.acme.com` is checked in DNS as entered. RDAP still uses `acme.com`, and DMARC falls back to `acme.com`'s policy (`dmarcInheritedFrom`). |
| `maxResults` | 0 (no limit) | Stops after this many charged domain rows. |
| `onlyNew` + `monitorId` | false | Monitoring mode, described below. |
| `maxConcurrency` | 10 | Number of domains looked up in parallel. |
| `rdapRequestsPerSecond` | 1 (max 2) | Politeness limit **per registry RDAP server**. |

### Output

Here is a real row from a test run (20 Oct 2026), with the TXT list shortened:

```json
{
  "recordType": "domain",
  "domain": "stripe.com",
  "registered": true,
  "rdapStatus": "ok",
  "rdapServer": "rdap.verisign.com",
  "registrar": "SafeNames Ltd.",
  "registrarIanaId": "447",
  "createdAt": "1995-09-12T04:00:00+00:00",
  "updatedAt": "2025-10-01T01:39:51+00:00",
  "expiresAt": "2027-09-11T04:00:00+00:00",
  "domainAgeYears": 31.1,
  "daysUntilExpiry": 325,
  "domainStatus": ["client delete prohibited", "client transfer prohibited", "client update prohibited", "server delete prohibited", "server transfer prohibited", "server update prohibited"],
  "rdapNameservers": ["ns-1087.awsdns-07.org", "ns-1882.awsdns-43.co.uk", "ns-423.awsdns-52.com", "ns-705.awsdns-24.net"],
  "dnssec": false,
  "contactsRedacted": true,
  "redactedContactRoles": ["abuse"],
  "aRecords": ["198.137.150.231", "198.202.176.231"],
  "mxRecords": [{"host": "aspmx.l.google.com", "priority": 10}, {"host": "alt1.aspmx.l.google.com", "priority": 20}],
  "nsRecords": ["ns-1087.awsdns-07.org", "ns-1882.awsdns-43.co.uk", "ns-423.awsdns-52.com", "ns-705.awsdns-24.net"],
  "dnsProvider": "Amazon Route 53",
  "txtRecords": ["MS=ms80697640", "…"],
  "txtVerifications": ["Microsoft 365", "Atlassian", "OpenAI", "Stripe", "…"],
  "mailProvider": "Google Workspace",
  "mailProviderSource": "MX",
  "spfRecord": "v=spf1 ip4:198.2.180.60/32 ip4:13.111.2.227/32 include:spf1.stripe.com include:greenhouse-outbound-mail.stripe.com include:_spf.qualtrics.com ~all",
  "spfPolicy": "softfail (~all)",
  "spfServices": ["Stripe", "Qualtrics"],
  "dmarcRecord": "v=DMARC1; p=reject; pct=100; fo=1; rua=mailto:***@stripe.com; ruf=mailto:***@stripe.com;",
  "dmarcPolicy": "reject",
  "dkimSelectors": ["google", "s1", "s2", "mandrill"],
  "dkimProviders": ["Google Workspace", "Mailchimp Transactional (Mandrill)", "SendGrid"],
  "mtaSts": false,
  "emailSecurityGrade": "A",
  "partial": false,
  "warnings": [],
  "checkedAt": "2026-10-20T13:01:35.326564+00:00"
}
```

Every domain row has the same columns (`null` where a value wasn't looked up), so CSV and Excel exports line up. The dataset has five views: **Overview**, **Registration (RDAP)**, **DNS records**, **Email security** and **Errors**. `RUN_SUMMARY` in the key-value store holds the totals, error codes, DNS query count and RDAP request count.

**Grade:** A means strict SPF (`-all` or `~all`), DMARC `quarantine` or `reject` at 100 %, and DKIM found. B means DMARC is enforced but either SPF is weak or no DKIM was found on the probed selectors. C means SPF and DMARC are both present but DMARC is not enforced. D means only one of SPF or DMARC is present. F means neither.

#### Good to know (limits)

- **RDAP coverage:** RDAP covers all gTLDs (.com, .net, .org, .app, .dev and so on) and the ccTLDs whose registry publishes an RDAP server in the IANA bootstrap. Some ccTLDs have no RDAP server there, including **.de, .io, .me and .so** at the time of writing. For those, `rdapStatus` is `unsupported_tld`, and the row still has all DNS and email data. We don't fall back to WHOIS scraping.
- **robots.txt is obeyed on RDAP servers too.** If a registry's robots.txt can't be fetched, the RFC treats that as "disallow", so the lookup returns `rdapStatus: "robots_disallowed"`. In our tests this happened with Nominet (**.uk**).
- **DKIM can't be listed.** Selectors are found by probing common names. A domain can sign with a selector we didn't try. If a zone answers *every* selector (a wildcard), `dkimWildcard` is true and no selectors are reported.
- **Politeness:** each RDAP server gets at most `rdapRequestsPerSecond` requests (default 1 per second). The Actor slows down after an HTTP 429. It leaves a server alone for the rest of the run if the server asks for a pause longer than 60 s, and after 5 failures in a row. In practice, about 1,000 `.com` domains take roughly 17 minutes, because they all go to Verisign's server. DNS goes through DNS-over-HTTPS (Cloudflare, with Google as the fallback).
- **Partial rows:** if RDAP fails but DNS works (or the other way round), you still get the row with `partial: true` and the reason in `warnings`.

### Monitoring: only new or changed domains

Set `onlyNew: true` and a `monitorId`, then schedule the Actor. A domain is output (and charged) only when:

- it has never been delivered for that monitor (`changeStatus: "new"`), or
- its registrar, expiry, status, registry nameservers, MX, NS, TXT, SPF, DMARC, DKIM selectors or mail provider changed (`changeStatus: "changed"`).

Unchanged domains are skipped and **not charged**. A and AAAA changes are ignored, because CDNs rotate IP addresses on every lookup.

State is kept in your named key-value store `signalscouter-bulk-domain-lookup-state`. It is saved on Apify's PERSIST_STATE, MIGRATING and ABORTING events, every 30 s, after each delivered domain in the **last 90 s before the run's timeout**, and at the end. That way a timed-out or aborted run doesn't re-deliver or re-charge. In a platform test, a run killed by a 20 s timeout and its rerun delivered 18 + 32 = 50 domains with **0 overlap**. Domains cut off by `maxResults` or your charge limit aren't remembered, so they come back next time. Entries are kept for 180 days.

### Pricing

Pay per event: you pay per **domain result** row. You don't pay for:

- error rows (invalid input, unregistered or non-existent domains, failed lookups)
- duplicates
- unchanged domains in monitoring mode

The run stops when your **maximum total charge** is reached and never goes over it.

| Error code | Meaning (free) |
|---|---|
| `INVALID_INPUT` | Not a domain, URL or email address |
| `DOMAIN_NOT_REGISTERED` | RDAP returns 404 and DNS returns NXDOMAIN |
| `DOMAIN_NOT_FOUND` | DNS returns NXDOMAIN and the TLD has no RDAP server |
| `LOOKUP_FAILED` | DNS and RDAP both failed. Try again later. After 25 failures in a row, the run stops early instead of continuing. |

### How this differs from our Company Tech Stack Actor

[Company Tech Stack & Domain Signals](https://apify.com/signalscouter/company-tech-stack-signals) **visits each company's website** to detect its technologies and hiring platform, and adds a summary of DNS, email and RDAP data. **Bulk Domain Lookup** doesn't fetch websites at all. It is a faster, cheaper **domain-records tool**, with raw A, AAAA, MX, NS and TXT records, full SPF and DMARC records, and registrar, expiry and status monitoring. Use it for domain portfolios and email-security audits. Use the tech-stack Actor for sales prospecting.

### More signalscouter Actors

| Actor | What it gives you |
|---|---|
| [Career Site Jobs Scraper](https://apify.com/signalscouter/career-site-jobs-scraper) | Jobs from 12 ATSes (Greenhouse, Lever, Ashby, Workday, Workable and more) in one schema, from company domains |
| [Workday Jobs Scraper](https://apify.com/signalscouter/workday-jobs-scraper) | Jobs from any `myworkdayjobs.com` career site, including sites with more than 2,000 jobs |
| [Greenhouse Jobs Scraper](https://apify.com/signalscouter/greenhouse-jobs-scraper) | Greenhouse boards with departments, offices, custom fields and pay ranges |
| [Ashby & Lever Jobs Scraper](https://apify.com/signalscouter/ashby-lever-jobs-scraper) | Startup jobs with structured compensation from Ashby and Lever |
| [UK Public Sector Jobs Scraper](https://apify.com/signalscouter/uk-public-sector-jobs-scraper) | NHS Jobs, Teaching Vacancies and DWP Find a Job |
| [Company Tech Stack & Domain Signals](https://apify.com/signalscouter/company-tech-stack-signals) | What a company's website runs on, plus DNS, email security and domain age |
| [SEC Form D Funding Signals](https://apify.com/signalscouter/sec-form-d-funding-signals) | Newly funded US companies from SEC Form D filings |
| [US Federal Contracts & Awards Signals](https://apify.com/signalscouter/us-federal-contracts-signals) | SAM.gov opportunities with incumbent award history, plus Grants.gov |

### Data sources and fair use

- **RDAP:** each registry's public RDAP server, found through the IANA bootstrap. Registries' terms say the data is for lawful, informational use and must not be used for unsolicited marketing. Respect them. Contacts are removed in any case.
- **DNS:** public DNS-over-HTTPS resolvers (Cloudflare and Google).
- Requests identify themselves as `signalscouter-bulk-domain-lookup/0.1 (+https://apify.com/signalscouter; signalscouter.data@gmail.com)`.

Questions or a missing field? Email **signalscouter.data@gmail.com** or open an issue on the Actor's Issues tab.

# Actor input Schema

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

Domains to look up, one per line (commas also work). URLs (https://www.acme.com/about) and e-mail addresses (jane@acme.com) are reduced to their domain; duplicates are merged into one row and one charge.

## `includeRdap` (type: `boolean`):

Registrar, creation/expiry dates, status and nameservers from the registry's public RDAP server (found via the IANA bootstrap). Registrant and other contacts are always removed.

## `includeEmailSecurity` (type: `boolean`):

SPF, DMARC, DKIM (probes common selectors), MTA-STS, TLS-RPT, BIMI and an A-F grade.

## `includeTxtRecords` (type: `boolean`):

Output the apex TXT records (e-mail addresses masked). SaaS verifications are detected either way.

## `keepSubdomains` (type: `boolean`):

Off: mail.acme.com is looked up as acme.com. On: DNS is checked for mail.acme.com itself (RDAP still uses acme.com, DMARC falls back to acme.com's policy).

## `maxResults` (type: `integer`):

Stop after this many charged domain rows (0 = no limit). The run also stops when your maximum total charge is reached.

## `onlyNew` (type: `boolean`):

Output (and charge) only domains this monitor hasn't delivered before, or whose registrar, expiry, status, nameservers, MX, NS, TXT, SPF, DMARC, DKIM or mail provider changed since. IP changes (A/AAAA) are ignored because CDNs rotate them. State is saved in your named key-value store 'signalscouter-bulk-domain-lookup-state' during the run, so a timed-out or aborted run doesn't re-deliver or re-charge.

## `monitorId` (type: `string`):

Separate state for different saved tasks, e.g. 'competitors'.

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

Domains looked up at once. Requests to each RDAP server are queued separately and rate-limited.

## `rdapRequestsPerSecond` (type: `number`):

Politeness limit for each registry's RDAP server (e.g. Verisign for .com). The Actor slows down further on HTTP 429 and pauses a server that asks for a long wait.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "github.com",
    "example.org"
  ],
  "includeRdap": true,
  "includeEmailSecurity": true,
  "includeTxtRecords": true,
  "keepSubdomains": false,
  "maxResults": 0,
  "onlyNew": false,
  "monitorId": "default",
  "maxConcurrency": 10,
  "rdapRequestsPerSecond": 1
}
```

# Actor output Schema

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

Registrar, dates, DNS host, mail provider and e-mail security grade per domain.

## `registration` (type: `string`):

Registrar, dates, status and nameservers.

## `dns` (type: `string`):

A, AAAA, MX, NS and TXT records.

## `email` (type: `string`):

SPF, DMARC, DKIM, MTA-STS, TLS-RPT, BIMI and mail provider.

## `errors` (type: `string`):

Inputs that couldn't be looked up. Never charged.

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

RUN_SUMMARY: totals, error codes, DNS queries, RDAP requests.

# 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": [
        "stripe.com",
        "github.com",
        "example.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("signalscouter/bulk-domain-lookup").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": [
        "stripe.com",
        "github.com",
        "example.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("signalscouter/bulk-domain-lookup").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": [
    "stripe.com",
    "github.com",
    "example.org"
  ]
}' |
apify call signalscouter/bulk-domain-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,signalscouter/bulk-domain-lookup"
        }
    }
}
```

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/YpupeHPZs6nCSYbtt/builds/vHXG4QYR5wnUoBOxR/openapi.json
