# WHOIS Lookup, DNS Records Scraper - DMARC, SPF, DKIM Checker (`snow_leo_data/whois-dns-lookup`) Actor

Bulk domain lookup in one run: WHOIS and RDAP registrar with registration and expiry dates, DNS records including A, AAAA, MX, NS, TXT, CAA, and the SSL certificate issuer with days until expiry. Email security reads SPF, DKIM and DMARC. 1200 zones over RDAP plus 238 more over WHOIS port 43.

- **URL**: https://apify.com/snow\_leo\_data/whois-dns-lookup.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** Developer tools, MCP servers, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.90 / 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

## DNS Records API — Bulk Domain Lookup, Email Deliverability

Give it a list of domains. Get back one row per domain containing the DNS zone,
the full email-authentication posture, the registration record and the TLS
certificate — **80 fields**, checked against the standards rather than merely
copied out of the resolver.

Run it once and it is a bulk lookup. Run it on a schedule with monitor mode on
and it becomes a drift alert: only the domains whose records actually moved come
back, each carrying the list of fields that changed and what they used to be.

Everything comes from public sources reached with plain HTTPS, DNS-over-HTTPS and
WHOIS on port 43. No API keys, no proxies, no browser, no third-party paid feed.

***

### What it collects

#### What DNS records does it return?

Seven record types for every domain — `A`, `AAAA`, `MX`, `NS`, `TXT`, `SOA`,
`CAA` — plus the CNAME chain, reverse DNS on request, and the DNSSEC delegation.

The CNAME chain is kept **separate** from the address records, and that is not a
cosmetic decision. Asking a resolver for the `A` record of a `www` host usually
returns the chain first: measured on 12 September 2026, `www.github.com` answers
with `[CNAME, A]` and `www.airbnb.com` with two CNAMEs followed by nine
addresses. Any tool that reads the first answer and calls it an address prints
`github.com` in the column marked "IP". Here `cname_chain` holds the aliases and
`a_records` holds addresses only.

`TXT` records are reassembled correctly. A TXT string longer than 255 bytes is
stored in DNS as several character-strings, and resolvers disagree about how to
show them: on `google._domainkey.apify.com`, Google returns 408 characters with
no quotes while Cloudflare returns 413 characters containing four quotes — the
same key in two chunks. Stripping the quotes naively inserts a **space in the
middle of the base64 public key**, which produces a DKIM key that looks right,
has almost the right length, and is worthless. This Actor concatenates the chunks
with nothing between them, the way DNS itself does.

#### How is the SPF record checked?

Not by printing it. SPF is resolved through its entire `include` tree and the
number of DNS lookups it forces on the receiving server is counted against the
limit of **10** set by RFC 7208 §4.6.4. Past that limit the record is a permanent
error and receivers ignore it altogether — the domain's mail starts failing SPF
with no error message to anybody.

Measured on the Tranco top 300 on 12 September 2026: five domains (1.7 %) are
already over the limit, the worst of them needing **24 lookups**. Median lookup
count is 2, the 90th percentile is 8. Two domains publish more than one SPF
record, which RFC 7208 §3.2 also makes a permanent error. None of that is
visible in the record text, which is exactly why tools that only show the text
report those domains as fine.

The row carries `spf_dns_lookups`, `spf_lookup_limit`,
`spf_lookup_limit_exceeded`, the flattened `spf_includes` list, the `all`
qualifier, and `spf_senders` — the third-party services allowed to send mail for
the domain, recognised from the include tree.

#### How are DMARC, DKIM and the rest reported?

**DMARC** is parsed into fields, not left as a string: `dmarc_policy`,
`dmarc_subdomain_policy`, `dmarc_percent`, `dmarc_rua`, `dmarc_ruf` and both
alignment modes. From those comes `spoofable`, which answers the only question
most buyers are actually asking. It is decided by DMARC, not by SPF: a domain
with a strict `-all` SPF and no DMARC record can still be spoofed in the `From:`
header, because SPF checks the envelope. On the Tranco top 300, 79 domains
(26.3 %) publish no DMARC at all and another 31 (10.3 %) publish `p=none`, which
only monitors.

**DKIM** selector names cannot be listed from DNS — they are known only to the
sender and the receiver — so they must be guessed. The default list of 15
selectors was chosen by measurement: probing 35 selectors across the Tranco top
120 found at least one key for 53 domains (44 %), and these 15 produced almost
all of the hits. The full list of 46 is available in the input; it costs roughly
three times the queries for a few percent more coverage.

**Wildcard zones are detected before the probing starts.** Some zones answer
*every* name under `_domainkey` with a CNAME. Measured: `bing.com` returns a
CNAME for `zzq7x9-no-such-selector._domainkey.bing.com`, and a naive prober
therefore "finds" all 35 selectors there. And this is not one odd domain —
**13 of the Tranco top 150 (8.7 %) behave that way** in a live run. This Actor
asks for a selector that cannot exist first; if the zone answers, it sets
`dkim_wildcard: true` and returns **no** selectors rather than dozens of
invented ones. The run report counts those zones as `dkim_wildcard_zones`.

**MTA-STS** is checked in both halves. The DNS record only announces that a
policy exists; the policy applies only if the file at
`https://mta-sts.<domain>/.well-known/mta-sts.txt` actually loads and says
`mode: enforce`. A domain with the record and without the file is protected
exactly as much as a domain with neither, and `mta_sts_no_policy` is a separate
finding for precisely that gap.

**TLS-RPT**, **BIMI** and **DNSSEC** round out the set. DNSSEC looks for a `DS`
delegation at the parent as well as the resolver's authenticated-data flag,
because the flag alone says the resolver validated something, not that the zone
is signed.

#### Which TLDs does the registration lookup cover?

All of them that publish anything, and getting there takes two different
protocols.

RDAP is the modern replacement for WHOIS and returns structured JSON. Counted
from IANA's own files on 12 September 2026: the root zone holds **1438** TLDs and
**1200** of them have an RDAP server, leaving **238 (16.6 %) with none**. That
remainder is not obscure — it includes `.de`, `.io`, `.co`, `.ch`, `.it`, `.eu`,
`.ru`, `.jp`, `.cn`, `.se`, `.es` and `.kz`. Verified directly:
`rdap.org/domain/example.de` and `rdap.org/domain/github.io` both answer HTTP
404\. A tool built on RDAP alone knows nothing about two of the most common
startup TLDs in existence.

Measured on a live cloud run over the Tranco top 150: RDAP answered for 131
domains, **WHOIS on port 43 answered for 14 more** (9.3 %), and 5 had no
registration data anywhere. Those 14 rows are exactly what an RDAP-only tool
leaves blank.

So for those zones the Actor falls back to **WHOIS on port 43**. The responsible
server is discovered from `whois.iana.org` rather than from a hardcoded table —
measured: `.de` resolves to `whois.denic.de`, `.io` to `whois.nic.io`, `.co` to
`whois.registry.co` — and the answer is parsed into the same fields RDAP
produces. `registration_source` in every row tells you which path answered.

The text parser handles the formats that break naive parsers. `.it` returns
`Created:` twice — once for the domain and once for the registrant, the latter
24 years later — so the first occurrence wins. `.eu` writes in blocks: the
`Registrar:` line is empty and the name sits below it as `Name:`; the parser
tracks which block it is inside, and picks up `Name:` **only** under the
registrar, because under the registrant the same key holds a person's name.

#### What is never collected?

Registrant, administrative, technical and billing contact data — names, email
addresses, telephone numbers, postal addresses. Not when RDAP returns it, not
when a WHOIS server prints it. The fields this Actor extracts are the registrar
(an organisation), the dates, the registry status codes, the transfer lock and
the registry nameservers.

#### What does the TLS certificate check do?

It opens a real TLS handshake on port 443 **with verification enabled** and
reports issuer, subject, validity window, protocol version, serial and the
number of SANs, plus `tls_days_until_expiry`.

A certificate that fails verification — expired, self-signed, issued for a
different name — is reported as the finding `tls-invalid`, not as a missing
certificate. That distinction matters: a silent empty field would tell you the
domain has no certificate when in fact it has a broken one, which is the thing
you were looking for. A domain that simply does not listen on 443 gets
`tls_error` with the connection failure and no finding, because not serving
HTTPS is not a certificate problem.

***

### The posture score

#### How is the score calculated?

Every domain starts at 100. Each finding subtracts its published weight and the
result is clamped to the 0–100 range. The weights are a convention, not a
measurement, so the whole table is printed here — a score whose rule is secret
tells you nothing.

| Finding | Weight | Severity |
|---|---:|---|
| `dmarc-missing` | 30 | critical |
| `spf-missing` | 25 | high |
| `spf-all-pass` | 25 | critical |
| `spf-lookup-limit` | 20 | critical |
| `dmarc-policy-none` | 20 | high |
| `tls-invalid` | 20 | critical |
| `spf-multiple-records` | 15 | high |
| `expiry-past` | 15 | critical |
| `spf-all-neutral` | 10 | medium |
| `dkim-none-found` | 10 | medium |
| `expiry-soon` | 10 | high |
| `tls-expiry-soon` | 10 | high |
| `spf-all-softfail` | 5 | low |
| `spf-include-broken` | 5 | medium |
| `dmarc-policy-quarantine` | 5 | low |
| `dmarc-partial-pct` | 5 | medium |
| `dmarc-no-reports` | 5 | low |
| `dnssec-unsigned` | 5 | low |
| `mta-sts-no-policy` | 5 | medium |
| `transfer-unlocked` | 5 | medium |
| `nameserver-mismatch` | 5 | medium |
| `caa-missing` | 3 | low |
| `mta-sts-missing` | 3 | low |
| `mta-sts-testing` | 2 | low |
| `mx-missing` | 0 | low |

Grades: **A+** from 95, **A** from 85, **B** from 70, **C** from 55, **D** from
40, **F** below that. `expiry-soon` fires inside 30 days, `tls-expiry-soon`
inside 14.

A finding is only raised for data that was actually collected. Turning off the
registration section does not make every domain look transfer-unlocked, and
turning off DKIM probing does not accuse every domain of having no key.

***

### Monitor mode

#### What does monitor mode actually remember?

Twenty fields per domain, stored in a **named** key-value store so the memory
survives between runs: the CNAME chain, MX hosts, nameservers, TXT, CAA, the SPF
record and its over-limit flag, the DMARC record and policy, the DKIM selectors
found, MTA-STS mode, DNSSEC state, registrar, domain expiry date, registry
status, registry nameservers, mail provider, DNS provider, the certificate
expiry date and the posture score.

Each returned row carries `change_type` (`NEW`, `UPDATED`, `UNCHANGED`),
`changed_fields` listing exactly which of the twenty moved, and
`previous_values` holding the earlier value for the five short fields worth
keeping in full — DMARC policy, posture score, expiry date, registrar and mail
provider.

Unchanged domains are not written to the dataset, so a daily run over a stable
list costs you nothing in result rows. Set `emitUnchanged` if you want them
anyway.

#### Why are A and AAAA records not watched by default?

Because they move on their own. Two cloud runs nine seconds apart reported four
of five domains as changed, and in every case the only difference was the
address set. Checked again with four snapshots ten seconds apart across the ten
prefill domains: three of them returned a different address set — `apify.com` on
AAAA, `stripe.com` and `vercel.com` on A — while `MX`, `NS`, `TXT` and `CAA` did
not move for a single domain out of the Tranco top 100.

That is load balancing, not a zone edit. Watching it would make anyone
monitoring CDN-backed domains pay for their whole list every single run. If a
change of address is what you are after, `watchAddressRecords` adds `a_records`
and `aaaa_records` to the watch list.

#### Why do list order changes not count as changes?

Because resolvers shuffle. The order of `A` records and `TXT` strings differs
between two consecutive queries — that is load balancing, not a zone edit. Lists
are normalised by sorting before fingerprinting; without that, every domain would
look updated on every run and you would pay for the entire list daily.

The same care applies to new fields: a field that was not in the previous run's
memory is not reported as changed, otherwise one update to this Actor would mark
every domain in your list as updated and bill you for all of them at once.

Memory holds 20 000 domains. Beyond that the oldest entries are dropped first.

#### What can monitor mode tell me that a lookup cannot?

An MX change means a mail migration just happened — the best possible moment for
a vendor conversation. A weakened DMARC policy means someone turned enforcement
off, usually while debugging a campaign, and often forgot to turn it back on. A
changed nameserver set with no corresponding registry change is what a hijack
looks like. A registrar change on a portfolio you do not own is a competitor
consolidating. An approaching expiry date is a renewal you are about to miss.

***

### Filters, and the reason they are careful

#### Do filters cost me money?

No. Every filter runs after the lookups and **before** the dataset write, so a
domain removed by a filter is never charged.

#### Can a filter return nothing by accident?

Two protections make that unlikely. First, choosing a finding code that can only
come from a section you switched off turns that section back on for the run and
says so in the log — asking for `transfer-unlocked` with registration disabled
would otherwise return zero rows from a perfectly green run.

Second, a filter never drops a domain merely because a value is **unknown**.
`expiringWithinDays` keeps domains whose expiry date could not be determined —
some zones publish none and some WHOIS servers rate-limit — and counts them
separately in the run report as `_kept_expiry_unknown`. Dropping them would
quietly assert that they are safe.

When a run does legitimately produce nothing, it says why: the report carries
`empty_reason` (`nothing-changed`, `filtered-out`, `source-returned-nothing`,
`charge-limit-reached`) and the log carries a warning naming the number.

***

### Output shape

#### What do I get per row?

80 fields in the default shape, arranged into five dataset views: **Domains**
(the decision columns), **Email security**, **DNS records**, **Registration** and
**TLS certificates**.

Monitor mode adds three more: `change_type`, `changed_fields` and
`previous_values`.

`compactOutput` reduces the row to 26 decision fields for AI agents and
spreadsheets. `excludeEmptyFields` drops nulls and empty lists. Neither changes
which domains are checked or charged — they are shape, not filter.

An empty cell always means the domain does not publish that record. Nothing is
inferred, estimated or filled in from a model.

#### How fast is it and how is the load handled?

Eight domains are processed in parallel; each domain costs roughly 40 DNS
queries plus optional WHOIS and TLS connections. Measured on the platform: the
built-in ten-domain sample finishes in **4.9 seconds**, and 150 domains from the
Tranco list in **284 seconds** — about 1.9 seconds per domain once WHOIS
rate-limits and unreachable HTTPS ports are in the mix. Results are written and the monitor memory is
saved every 40 domains, so an interrupted run loses at most one batch — and the
order is always deliver first, record second, never the reverse.

Rate limiting is shared across threads by host: when a host asks for a pause,
every thread waits, rather than the sleeping one waiting while the rest keep
hammering.

***

### Honest limits

- **WHOIS servers rate-limit.** Measured: `whois.domain-registry.nl` stopped
  answering after a handful of queries in one session. Large batches in the
  non-RDAP zones will produce some rows with `registration_error` filled in
  instead of dates. RDAP zones — 1200 of the 1438 — do not have this problem.
- **DKIM coverage is a probe, not an enumeration.** 44 % of the Tranco top 120
  yielded a key to 35 selectors. An empty `dkim_selectors_found` means "not among
  the selectors tried", not "no DKIM".
- **Many domains do not serve HTTPS on the apex.** In the Tranco top 150 run,
  36 domains (24 %) could not be connected to on port 443 at all. Those get
  `tls_error` with the connection failure and **no** finding — not serving HTTPS
  is not a certificate problem. Three domains in the same run had a certificate
  that failed verification, and those are findings.
- **No HTTP status or page content.** This Actor inspects DNS, registry and the
  TLS certificate. It does not fetch the website.
- **No Standby / instant-API mode.** Standby would exclude this Actor from
  Apify's agentic-payments listing, which is worth more than the convenience.
- **IPv6 reverse DNS is not looked up.** The nibble form produces a 72-character
  query name that is almost never populated, and it would cost a query per
  address.
- **Internationalised domains must be supplied in punycode.** Raw Unicode names
  are rejected with a warning rather than silently mangled.
- **Certificate Transparency logs are not queried.** `crt.sh` answered HTTP 502
  to a wildcard query during testing and took 14.7 seconds for a single domain;
  it is not dependable enough for a run that must finish.

***

### FAQ

#### Do I need an API key for anything?

No. DNS-over-HTTPS at Google and Cloudflare, the IANA RDAP bootstrap, the RDAP
servers themselves, `whois.iana.org`, the registry WHOIS servers and the MTA-STS
policy files are all public and unauthenticated. Nothing here needs a proxy or a
headless browser either.

#### What happens if a domain does not exist?

The row still comes back, with `resolved: false` and `dns_status: NXDOMAIN`. Use
`onlyResolved` to drop those. A domain that answers NOERROR with no `A` record —
common for mail-only domains — counts as resolved if it has `MX`, `NS` or `TXT`.

#### Can I feed it URLs or email addresses instead of domains?

Yes. `https://www.example.co.uk/pricing?ref=1` and `sales@example.co.uk` are both
accepted; schemes, paths, queries, ports, trailing dots and letter case are
normalised away. Duplicates are removed before anything is charged, and anything
that cannot be read as a domain is reported in the log and counted in the report
rather than silently dropped.

#### How do I use it as a daily email-security monitor?

Put your domain list in, switch on **Monitor mode**, and schedule the Actor
daily. The first run returns everything as `NEW` and fills the memory. Later runs
return only the domains that moved. Combine with `minSeverity: high` to be told
only about the serious drift.

#### How do I find spoofable domains in a prospect list?

Switch on `onlySpoofable`, or filter on `findingCodes` for `dmarc-missing` and
`dmarc-policy-none`. Every returned row already carries the posture score, the
worst severity and the finding list, so the output sorts itself.

#### How do I watch a domain portfolio for expiry and hijacking?

Use `expiringWithinDays` for renewals, and monitor mode for the rest: a change in
`registry_status`, `registry_nameservers` or `registrar` is what an unauthorised
transfer looks like. `transfer_locked` tells you whether the registry lock is
even in place.

#### Why does one of my domains show a lower score than I expect?

Open `findings` on that row. Every entry names the code, the severity, the weight
it subtracted and a sentence explaining it. The score is only the sum; the
findings are the substance.

#### Does the daily run cost the same as the first one?

In monitor mode, no. Only changed domains are written, and only written rows are
billable results. A stable list of a thousand domains that nobody touched returns
zero rows and says `empty_reason: nothing-changed`.

#### What is in the run report?

`REPORT` in the key-value store: domains requested and checked, rows written,
unusable input entries, which source answered for registration, how many zones
answer DKIM probes with a wildcard, how many SPF records are over the RFC limit,
how many TLS certificates failed verification versus were unreachable, the
change counts, and exactly what each filter removed before you were charged.

#### What else is this called?

Names buyers use: WHOIS bulk lookup, DNS records scraper, DMARC checker, DMARC policy audit, SPF record checker, SPF lookup limit, DKIM selector discovery, MX record lookup, WHOIS expiry alert, domain expiry monitor, RDAP lookup API. All of them read the same record set this Actor returns.

Other names for what this returns: reverse DNS PTR lookup, DMARC RUA aggregate reports, SMTP TLS reporting, DNSSEC validation check, SPF too many DNS lookups, domain expiration date checker, SSL certificate expiry alert and SSL expiry monitor, SaaS vendor detection over DNS, email security posture score, registrar IANA id, MX record health check. Each of those is a field in the row: `reverse_dns`, `dmarc_rua`, `tls_rpt_record`, `dnssec_signed`, `spf_dns_lookups`, `expires_at`, `tls_days_until_expiry`, `verified_services`, `posture_grade`, `registrar_iana_id`, `mx_records`.

# Actor input Schema

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

One domain per line. URLs and email addresses are accepted and cleaned automatically: https://www.example.co.uk/pricing and sales@example.co.uk both become example.co.uk. Duplicates are removed before anything is charged.

## `maxItems` (type: `integer`):

Hard stop on how many domains are checked. 0 means every domain in the list. Leaving the list empty runs a 10-domain sample instead of a full job.

## `includeRegistration` (type: `boolean`):

Registrar, creation and expiry dates, registry status codes, transfer lock and registry nameservers. RDAP covers 1200 TLDs; the other 238 in the IANA root (.de, .io, .co, .ch, .it, .eu, .ru, .jp, .cn, .se, .es, .kz and more) fall back to WHOIS on port 43. Registrant names, emails, phones and addresses are never collected.

## `includeDkim` (type: `boolean`):

DKIM selector names cannot be listed from DNS — only guessed. Measured on Tranco top-120: probing 35 selectors found a key for 53 domains (44%). Zones that answer every name with a wildcard are detected and reported instead of producing dozens of fake selectors.

## `dkimSelectorSet` (type: `string`):

Common = 15 selectors that produced almost all hits in the measurement. Full = 46 selectors, roughly three times slower for a few percent more.

## `dkimSelectors` (type: `array`):

Overrides the list above. Use this when you already know the selector, for example from a signed message header.

## `includeDnssec` (type: `boolean`):

Looks for a DS delegation at the parent as well as the resolver's authenticated-data flag. The flag alone only says the resolver validated something, not that the zone is signed.

## `includeTlsCertificate` (type: `boolean`):

Opens a TLS handshake on port 443 and reads the certificate: issuer, validity window, days left and how many SANs it covers. A certificate that exists but fails validation (expired, self-signed, issued for another name) is reported as a finding, not as missing data.

## `includeReverseDns` (type: `boolean`):

PTR records for up to 4 IPv4 addresses of the domain. Shows whose hardware the site sits on; on shared hosting hundreds of domains return the same name. Off by default because it costs an extra query per address.

## `requestTimeoutSecs` (type: `integer`):

Seconds to wait for one DNS, WHOIS or TLS request before giving up on it. The run continues; the field it would have filled stays empty.

## `onlyChanged` (type: `boolean`):

Remembers 20 fields per domain in a named key-value store and returns only domains where one of them moved: new MX, weakened DMARC, changed nameservers, a new registrar, a closer expiry date. Every row carries change\_type (NEW / UPDATED / UNCHANGED) and changed\_fields. Unchanged domains are not written, so you are not charged for them twice.

## `emitUnchanged` (type: `boolean`):

Writes UNCHANGED rows as well. Off by default because you would pay for the same data on every run.

## `watchAddressRecords` (type: `boolean`):

Off by default, and that is a measurement, not a guess: four snapshots ten seconds apart of the ten prefill domains showed a different address set for three of them (apify.com on AAAA, stripe.com and vercel.com on A), while MX, NS, TXT and CAA did not move for a single domain out of the Tranco top 100. Address rotation is load balancing, not a zone edit, and watching it makes you pay for CDN-backed domains every single run.

## `onlyWithFindings` (type: `boolean`):

All filters here run before rows are written, so filtered-out domains are never charged.

## `maxPostureScore` (type: `integer`):

Keep only domains scoring at or below this. 0 turns the filter off. The score starts at 100 and every finding subtracts its published weight; the full table is in the README.

## `minSeverity` (type: `string`):

Keep only domains whose worst finding is at least this severe.

## `findingCodes` (type: `array`):

Keep only domains carrying one of the chosen findings. Choosing a registration or DKIM finding switches the matching section on automatically, so the filter cannot silently return nothing.

## `onlySpoofable` (type: `boolean`):

A domain counts as spoofable when DMARC does not enforce: no record, p=none, or a policy applied to less than 100% of mail. SPF alone does not stop From: spoofing.

## `expiringWithinDays` (type: `integer`):

Keep only registrations expiring inside this window. 0 turns it off. Domains whose expiry date is genuinely unknown are kept and counted separately in the run report — dropping them would claim they are safe.

## `onlyResolved` (type: `boolean`):

Drops names that answer NXDOMAIN and have no MX, NS or TXT records.

## `compactOutput` (type: `boolean`):

22 decision fields instead of 60. Built for AI agents and spreadsheets that choke on wide rows.

## `excludeEmptyFields` (type: `boolean`):

Removes null and empty values from each row. Smaller JSON, but columns appear and disappear between rows.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "github.com",
    "stripe.com",
    "shopify.com",
    "cloudflare.com",
    "notion.so",
    "figma.com",
    "vercel.com",
    "linear.app",
    "openai.com"
  ],
  "maxItems": 0,
  "includeRegistration": true,
  "includeDkim": true,
  "dkimSelectorSet": "common",
  "dkimSelectors": [],
  "includeDnssec": true,
  "includeTlsCertificate": true,
  "includeReverseDns": false,
  "requestTimeoutSecs": 8,
  "onlyChanged": false,
  "emitUnchanged": false,
  "watchAddressRecords": false,
  "onlyWithFindings": false,
  "maxPostureScore": 0,
  "minSeverity": "any",
  "findingCodes": [],
  "onlySpoofable": false,
  "expiringWithinDays": 0,
  "onlyResolved": false,
  "compactOutput": false,
  "excludeEmptyFields": false
}
```

# Actor output Schema

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

All collected rows

# 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",
        "github.com",
        "stripe.com",
        "shopify.com",
        "cloudflare.com",
        "notion.so",
        "figma.com",
        "vercel.com",
        "linear.app",
        "openai.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/whois-dns-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": [
        "apify.com",
        "github.com",
        "stripe.com",
        "shopify.com",
        "cloudflare.com",
        "notion.so",
        "figma.com",
        "vercel.com",
        "linear.app",
        "openai.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/whois-dns-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": [
    "apify.com",
    "github.com",
    "stripe.com",
    "shopify.com",
    "cloudflare.com",
    "notion.so",
    "figma.com",
    "vercel.com",
    "linear.app",
    "openai.com"
  ]
}' |
apify call snow_leo_data/whois-dns-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/whois-dns-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/s9cvLYX7k5rp1o7hV/builds/as2c20cgkHhYqqj8n/openapi.json
