# Domain WHOIS, DNS & SSL Lookup (RDAP) (`antsonahill/domain-whois-dns-lookup`) Actor

Look up a list of domains: registrar, creation and expiry dates, status and nameservers over RDAP (the successor to WHOIS), DNS records with mail provider, SPF and DMARC, and the TLS certificate. One row per domain, with no personal contact data.

- **URL**: https://apify.com/antsonahill/domain-whois-dns-lookup.md
- **Developed by:** [Anton Sajin](https://apify.com/antsonahill) (community)
- **Categories:** Developer tools, SEO tools, Lead generation
- **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

## Domain WHOIS, DNS & SSL Lookup (RDAP)

Paste a list of domains and get one clean row per domain: who the registrar is and when the domain expires, how its DNS and e-mail are set up, and when its TLS certificate runs out.

Everything comes from the open protocols themselves, not from scraped lookup websites:

- **Registration data** from the domain registry over RDAP, the standard protocol that succeeds WHOIS.
- **DNS records** by direct resolution.
- **Certificate facts** from a direct TLS handshake with the host.

The output contains registrar and organisation-level data only. It never contains the name, e-mail address, phone number or postal address of a registrant or contact person.

### What you can do with it

- **Watch expiry dates.** Schedule the Actor weekly over your own domains and get `daysUntilExpiry` for both the domain and its certificate.
- **Enrich a company list.** From a domain alone: mail provider (Google Workspace, Microsoft 365, Proofpoint and others), DNS operator, hosting platform or CDN, and domain age.
- **Audit e-mail authentication.** See which domains have SPF and DMARC, and which DMARC policy they ask for.
- **Check whether names are taken.** Each domain gets an `availability` verdict.
- **Screen domains.** Age, registrar, status codes and DNSSEC for vendor checks and risk reviews.

### What each check returns

Turn each check on or off in the input. The price per domain is the same either way.

| Check | Fields |
| --- | --- |
| Registration (RDAP) | `registrar`, `registrarIanaId`, `registrarUrl`, the registrar's abuse mailbox and phone, `registrantOrganization` when the registry publishes one, `createdAt`, `updatedAt`, `expiresAt`, `domainAgeDays`, `daysUntilExpiry`, `statuses`, `nameservers`, `dnssecSigned` |
| DNS | `a`, `aaaa`, `cnameChain`, `mx`, `ns`, `txt`, `soa`, `caa`, plus `mailProvider`, `hasMx`, `hasSpf`, `spfRecord`, `hasDmarc`, `dmarcRecord`, `dmarcPolicy`, `dnsProvider`, `hostingProvider` |
| TLS certificate | `subject`, `issuer`, `issuerOrganization`, `validFrom`, `validTo`, `daysUntilExpiry`, `subjectAltNames`, `trusted`, `trustError`, `matchesHost`, `protocol`, `serialNumber`, `fingerprintSha256` |

### Which domains have registration data

Read this before you rely on the registration check. DNS and TLS work for every domain. Registration data does not, for two reasons.

**The Actor uses RDAP only.** It finds each TLD's server in the [IANA bootstrap registry](https://data.iana.org/rdap/dns.json). Some registries have not listed an RDAP server there, and the Actor does not fall back to old-style WHOIS or to scraping lookup pages.

**Each registry sets its own terms of use.** Some forbid passing their data on or using it commercially. The Actor queries only registries whose terms were read and allow lookups like this one.

As of October 2026 that gives:

| Registration data | TLDs |
| --- | --- |
| Available (555 TLDs) | `.com`, `.net`, `.org`, `.info`, `.ai`, `.app`, `.dev`, `.page`, `.pro`, `.live`, `.mobi`, `.asia`, `.in`, `.cc`, `.name`, `.online`, `.site`, `.store`, `.tech`, `.website`, `.space`, `.cloud`, `.top`, `.click`, and several hundred newer TLDs such as `.email`, `.agency`, `.company`, `.digital`, `.life`, `.world`, `.news`, `.media`, `.group`, `.studio`, `.solutions`, `.technology`, `.academy` |
| No RDAP server listed by IANA | `.io`, `.co`, `.me`, `.us`, `.de`, `.eu`, `.es`, `.it`, `.ch`, `.se`, `.dk`, `.be`, `.at`, `.ie`, `.ru`, `.cn`, `.jp`, `.nz`, `.mx`, `.za` and others |
| Registry terms do not allow it | `.uk`, `.xyz`, `.biz`, `.club`, `.tv`, `.br`, `.nl`, `.no`, `.cz`, `.is`, `.si` and other TLDs run by the same registries |
| Terms not reviewed yet | `.fr`, `.ca`, `.au`, `.shop`, `.pl`, `.fi`, `.kr`, `.sg` and others |

For a domain outside the first row, `registration.status` says which case applies and `registration.message` gives the reason in plain words. The rest of the row is still filled in, and `availability` is then worked out from DNS.

### Input

Put one domain per line in **Domains**. These all work:

- a plain name: `example.com`
- an internationalised name: `münchen.de` (or its punycode form `xn--mnchen-3ya.de`)
- a URL: `https://www.example.com/pricing`
- a host with a subdomain: `shop.example.co.uk`

URLs and subdomains are reduced to the registrable domain using the Public Suffix List, so `shop.example.co.uk` becomes `example.co.uk`, and a domain that appears twice is checked once.

```json
{
    "domains": ["stripe.com", "https://www.wikipedia.org/wiki/Domain_name", "python.org", "claude.ai", "münchen.de"],
    "checkRegistration": true,
    "checkDns": true,
    "checkTls": true,
    "keepSubdomains": false
}
```

| Field | What it does |
| --- | --- |
| `domains` | The list to check. |
| `checkRegistration` | Registration data over RDAP. On by default. |
| `checkDns` | DNS records and the facts derived from them. On by default. |
| `checkTls` | TLS certificate from a handshake on port 443. On by default. |
| `keepSubdomains` | Check each host exactly as entered instead of reducing it to the registrable domain. Use it to watch the certificates of several subdomains. Registration data still belongs to the registrable domain. Off by default. |

A line that is not a domain (free text, an IP address, an unknown TLD) never fails the run. It is listed with the reason in the `SUMMARY` record of the run's key-value store.

### Output

One item per domain. This one is from a real run:

```json
{
    "id": "python.org",
    "input": "python.org",
    "domain": "python.org",
    "domainUnicode": "python.org",
    "host": "python.org",
    "tld": "org",
    "publicSuffix": "org",
    "availability": "registered",
    "availabilitySource": "rdap",
    "registration": {
        "status": "found",
        "message": null,
        "rdapServer": "https://rdap.publicinterestregistry.org/rdap/",
        "registrar": "Gandi SAS",
        "registrarIanaId": 81,
        "registrarUrl": null,
        "registrarAbuseEmail": "abuse@support.gandi.net",
        "registrarAbusePhone": "+33.170377661",
        "registrantOrganization": null,
        "createdAt": "1995-03-27T05:00:00.545Z",
        "updatedAt": "2026-08-12T05:01:11.536Z",
        "expiresAt": "2033-03-28T05:00:00.000Z",
        "domainAgeDays": 11520,
        "daysUntilExpiry": 2360,
        "statuses": ["client transfer prohibited"],
        "nameservers": ["ns-1134.awsdns-13.org", "ns-2046.awsdns-63.co.uk", "ns-484.awsdns-60.com", "ns-981.awsdns-58.net"],
        "dnssecSigned": false
    },
    "dns": {
        "status": "ok",
        "message": null,
        "a": ["151.101.0.223", "151.101.128.223", "151.101.192.223", "151.101.64.223"],
        "aaaa": ["2a04:4e42:200::223", "2a04:4e42:400::223", "2a04:4e42:600::223", "2a04:4e42::223"],
        "cnameChain": [],
        "mx": [{ "priority": 50, "exchange": "mail.python.org" }],
        "ns": ["ns-1134.awsdns-13.org", "ns-2046.awsdns-63.co.uk", "ns-484.awsdns-60.com", "ns-981.awsdns-58.net"],
        "txt": [
            "MS=73147F1EC0843C399CF17F586EC6B8EAF8C57961",
            "status-page-domain-verification=9y2klhzbxsgk",
            "v=spf1 mx ip4:188.166.95.178/32 ip6:2a03:b0c0:2:d0::71:1 include:stspg-customer.com include:_spf.google.com include:mailgun.org ~all"
        ],
        "soa": {
            "primaryNameserver": "ns-2046.awsdns-63.co.uk",
            "serial": 1,
            "refresh": 7200,
            "retry": 900,
            "expire": 1209600,
            "minimumTtl": 86400
        },
        "caa": [],
        "wwwCnameChain": ["dualstack.python.map.fastly.net"],
        "hasMx": true,
        "mailProvider": null,
        "hasSpf": true,
        "spfRecord": "v=spf1 mx ip4:188.166.95.178/32 ip6:2a03:b0c0:2:d0::71:1 include:stspg-customer.com include:_spf.google.com include:mailgun.org ~all",
        "hasDmarc": true,
        "dmarcRecord": "v=DMARC1; p=none; pct=100; rua=mailto:re+shb8ybr70a3@dmarc.postmarkapp.com; sp=none; aspf=r;",
        "dmarcPolicy": "none",
        "dnsProvider": "Amazon Route 53",
        "hostingProvider": "Fastly"
    },
    "tls": {
        "status": "ok",
        "message": null,
        "host": "python.org",
        "subject": "www.python.org",
        "issuer": "GlobalSign Atlas R3 DV TLS CA 2025 Q4",
        "issuerOrganization": "GlobalSign nv-sa",
        "validFrom": "2026-01-13T13:03:46.000Z",
        "validTo": "2027-02-14T13:03:45.000Z",
        "daysUntilExpiry": 126,
        "subjectAltNames": ["www.python.org", "*.python.org", "python.org"],
        "trusted": true,
        "trustError": null,
        "matchesHost": true,
        "protocol": "TLSv1.3",
        "serialNumber": "01FC68FD084537B393B8D6C708974969",
        "fingerprintSha256": "A1:62:96:4C:FE:42:09:E3:08:F7:00:E8:80:28:75:7E:B8:3D:22:7B:2B:B3:5F:67:F1:86:A6:E7:0E:1E:20:1A"
    },
    "scrapedAt": "2026-10-11T04:54:43.463Z"
}
```

`txt` is shortened here to three of the thirteen records the run returned. Nothing else was changed.

#### Reading the result

**`availability`** is one of:

| Value | Meaning |
| --- | --- |
| `registered` | The registry has a record for the domain, or the domain has nameservers in DNS. |
| `available` | The registry answered that it has no such domain. A registry can answer the same way for a name it keeps in reserve, so confirm with a registrar before you count on it. |
| `likely_available` | No registry answer was possible, and the domain does not exist in DNS. Registered domains can be without DNS, so this is a hint only. |
| `unknown` | Not enough evidence either way. |

`availabilitySource` says whether the verdict rests on `rdap` or `dns`.

**A check that was turned off** leaves its object `null`.

**Each check has its own `status` and `message`**, so one failed check does not hide the others:

- `registration.status`: `found`, `not_found`, `no_rdap_server`, `registry_not_supported`, `rate_limited`, `error`
- `dns.status`: `ok`, `nxdomain` (the domain does not exist in DNS), `error`
- `tls.status`: `ok`, `no_tls` (port 443 refuses connections), `handshake_failed`, `unreachable`, `error`

**Empty is not the same as unknown.** A DNS record set is an empty list when the resolver answered that there is none, and `null` when its lookup failed. `hasSpf`, `hasDmarc` and `hasMx` follow the same rule: `false` means checked and absent, `null` means the lookup failed.

**Fields a source does not provide are `null`, never guessed.** `mailProvider`, `dnsProvider` and `hostingProvider` are filled in only when a record points at a host name the provider documents for its customers. In the example above the mail server is the organisation's own, so `mailProvider` is `null`. `hostingProvider` comes from CNAME records only, so a site that reaches its host through a plain A record gets `null`.

**Two sets of nameservers.** `registration.nameservers` is what the registry delegates the domain to. `dns.ns` is what DNS answers. They normally match; a difference is worth a look.

**`tls.trusted`** tells you whether the certificate verifies against the root certificates bundled with Node.js, which follow Mozilla's list. Certificates that fail are still described, with the reason in `trustError`, for example `CERT_HAS_EXPIRED`. If the bare domain does not answer on port 443, `www.` is tried, and `tls.host` says which one answered.

`id` is the checked host in ASCII form and is stable between runs, so you can use it as a key when you sync results into a database or spreadsheet.

### Privacy

RDAP servers still return personal contact details for some domains. The Actor does not read them into the output. From the registrant it takes an organisation name only, and only when the registry labels it as one. From the registrar's abuse contact it takes the mailbox and phone number, never the name of the person. The administrator mailbox in a domain's SOA record is left out for the same reason.

DNS records are returned as published. TXT and DMARC records can contain e-mail addresses that the domain owner chose to publish there.

### Pricing

Pay per result: **$2.00 per 1,000 domains** checked, whichever checks are on. There is no monthly fee.

You are not charged for input lines that are not domains, for duplicates, or for a domain where none of the checks you asked for gave an answer. Those are listed in the `SUMMARY` record instead of the dataset.

You can cap what a run may spend with the *Maximum cost per run* setting; the Actor stops cleanly when it reaches your limit.

### Speed and limits

DNS and TLS checks run 25 domains at a time. Registration lookups are deliberately slower: one request at a time per registry and at most two per second, because registries ask clients not to send high-volume automated queries. A list of mixed TLDs moves faster than a long list from one registry.

For the same reason, a run looks up registration data for its first 500 domains only. Domains after that still get their DNS and TLS checks, and the run's `SUMMARY` record says how many were past the limit. Put larger lists in separate runs.

If a registry signals that it is rate limiting, the Actor slows down, and if it keeps doing so the Actor stops asking that registry for a while. The affected domains get `registration.status` `rate_limited` and keep their DNS and TLS results. The Actor does not use proxies to get around a registry's limit.

### FAQ

**Is this WHOIS?** It is the same registry data, read over RDAP, the protocol that succeeds WHOIS. The Actor does not use the old WHOIS protocol on port 43, which is why TLDs without an RDAP server have no registration data here.

**Why is the registrant missing?** For most domains, registries no longer publish registrant details, and this Actor would not output a person's details even where they do. `registrantOrganization` is filled in when the registry publishes an organisation, which some do and many do not.

**What may I use the data for?** Registries provide RDAP for looking up information about domain registrations and forbid using it to send unsolicited marketing. Each registry's terms apply to the registration data you receive; the `SUMMARY` record of every run lists the registries that were asked, with a link to each one's terms.

**Where do the DNS answers come from?** From the resolvers of the Apify platform the run executes on, queried directly. Results reflect what those resolvers see at that moment.

**Can it check a certificate on another port, or a mail server's certificate?** No. The TLS check is for port 443 only.

**How fresh is the data?** Every run asks the live sources. Nothing is cached between runs.

**A TLD I need has no registration data.** Open an issue on the Issues tab and name the TLD. Registries are added once their terms have been read and allow it.

**Something looks wrong.** Open an issue on the Issues tab and include the run ID.

# Actor input Schema

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

One domain per line. Plain names (example.com), internationalised names (münchen.de) and full URLs (https://www.example.com/pricing) all work. URLs and subdomains are reduced to the registrable domain, so www.example.co.uk becomes example.co.uk, and duplicates are checked once.

## `checkRegistration` (type: `boolean`):

Registrar, creation, update and expiry dates, domain age, days until expiry, status codes, nameservers and DNSSEC, read from the domain registry over RDAP. Available for .com, .net, .org and several hundred other TLDs; for the rest the result says why there is no registry data. Looked up for the first 500 domains of a run.

## `checkDns` (type: `boolean`):

A, AAAA, CNAME, MX, NS, TXT, SOA and CAA records, plus the mail provider behind the MX records, whether SPF and DMARC are set up, who runs the nameservers, and the hosting platform or CDN when a CNAME shows it.

## `checkTls` (type: `boolean`):

Issuer, validity dates, days until expiry, the names the certificate covers and whether it is trusted, read from a TLS handshake on port 443. If the bare domain does not answer, www is tried.

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

By default shop.example.com is reduced to example.com. Turn this on to run the DNS and TLS checks on the exact host you entered, for example to watch the certificates of several subdomains. Registration data always belongs to the registrable domain.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "https://www.wikipedia.org/wiki/Domain_name",
    "python.org",
    "claude.ai",
    "münchen.de"
  ],
  "checkRegistration": true,
  "checkDns": true,
  "checkTls": true,
  "keepSubdomains": false
}
```

# Actor output Schema

## `domains` (type: `string`):

One item per domain with its registration data, DNS records and TLS certificate.

## `summary` (type: `string`):

Counts by result, the registries that were asked with links to their terms, and the input lines that were not domains or gave no answer, with the reason.

# 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",
        "https://www.wikipedia.org/wiki/Domain_name",
        "python.org",
        "claude.ai",
        "münchen.de"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("antsonahill/domain-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": [
        "stripe.com",
        "https://www.wikipedia.org/wiki/Domain_name",
        "python.org",
        "claude.ai",
        "münchen.de",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("antsonahill/domain-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": [
    "stripe.com",
    "https://www.wikipedia.org/wiki/Domain_name",
    "python.org",
    "claude.ai",
    "münchen.de"
  ]
}' |
apify call antsonahill/domain-whois-dns-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,antsonahill/domain-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/9x02blHDGC8XXI6LN/builds/3ckcA5RAzlEPLB06x/openapi.json
