# Domain RDAP & DNS Security Auditor (`kadev-code/domain-rdap-dns`) Actor

Audit domain registration, expiry, DNS records, MX, SPF, DMARC and DNSSEC using IANA RDAP and DNS over HTTPS.

- **URL**: https://apify.com/kadev-code/domain-rdap-dns.md
- **Developed by:** [Kadev Code](https://apify.com/kadev-code) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 domain auditeds

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 RDAP & DNS Security Auditor

Audit up to 1,000 domain names per run. Get registrar details, registration dates,
expiry, status and nameservers from the TLD's RDAP service. Check A, AAAA, MX, NS,
TXT, SPF, DMARC and DNSSEC evidence through DNS over HTTPS.

The Actor uses Python 3.12 and the Apify SDK. It does not require API keys or a
browser. The default memory allocation is 256 MB.

### Input

```json
{
  "domains": ["example.com", "https://bücher.de/catalog", "EXAMPLE.COM"],
  "checkDns": true,
  "dnsRecordTypes": ["A", "AAAA", "MX", "NS", "TXT"],
  "checkDmarc": true,
  "checkDnssec": true,
  "maxConcurrency": 5
}
```

`domains` is required. Names use IDNA/UTS 46 normalization and punycode. HTTP(S)
schemes, paths, query strings and fragments are removed. Credentials, ports, IP
addresses and invalid names are rejected. RDAP uses the registrable domain from
the offline Public Suffix List (PSL); DNS uses the normalized host requested.
For `https://www.example.co.uk/path`, RDAP queries `example.co.uk`, while DNS
queries `www.example.co.uk`. The bundled PSL snapshot is pinned through
`tldextract`; no PSL download or cache write occurs at runtime. Its ICANN rules,
including wildcard and exception rules, identify registrations; private hosting
suffixes such as `blogspot.com` do not create separate RDAP registrations.
Unlisted TLDs use the PSL implicit wildcard rule and still need an IANA RDAP route.
Public suffixes alone, such as `co.uk`, are rejected.
Equivalent hosts produce one result, with the first input index. Distinct hosts
under one registered domain remain separate DNS audits and billable items.
A run with empty input fails;
there is no automatic demo lookup.

| Option | Default | Effect |
| --- | --- | --- |
| `domains` | Required | 1–1,000 strings, at most 2,048 characters each |
| `checkDns` | `true` | `false` disables all DNS, DMARC and DS queries |
| `dnsRecordTypes` | All five types | Nonempty subset of A, AAAA, MX, NS and TXT |
| `checkDmarc` | `true` | Query TXT at `_dmarc.<domain>` |
| `checkDnssec` | `true` | Query DS at the domain |
| `maxConcurrency` | `5` | 1–10 workers; one active RDAP request per server, eight per DoH resolver |

Unknown options cause an input error. A missing or malformed RDAP service produces
an error for each affected domain. An invalid domain produces an error row; other
valid domains continue.

### Output

The default dataset contains one row per unique normalized domain, plus invalid
domain rows. `index` is the zero-based position in the original input. The
`.actor/output_schema.json` output link opens the dataset overview.

Successful rows have `success: true` and `error: null`, with these fields:

- `domain`, `inputDomain`: normalized punycode host requested, without the URL path.
- `registrableDomain`: PSL-derived domain used for RDAP.
- `partialData`, `dnsError`: `false` and `null` for a complete audit. A DNS failure
  or DNS deadline keeps the valid RDAP result and completed DNS answers, sets
  `partialData: true`, and lists the failed query keys in `dnsError.failedQueries`.
  Those DNS fields and their derived flags remain `null`; the item still has
  `success: true`, emits a `DNS_PARTIAL` log warning and incurs `domain-audited`.
- `registrar`: public registrar name and IANA registrar ID, when available.
- `createdAt`, `expiresAt`, `updatedAt`: UTC ISO dates, or `null` if unavailable.
- `daysToExpiry`: whole days, rounded down, from the lookup time; negative after expiry.
- `status`, `nameservers`: RDAP registration status and nameserver names.
- `dns`: A, AAAA, MX, NS and TXT arrays. MX entries retain their DNS presentation
  form, for example `10 mail.example.com.`. Adjacent TXT chunks are joined.
- `spf`, `hasSpf`, `hasMx`: SPF records and presence flags. Null MX (`0 .`) means
  `hasMx: false`. These fields do not prove email delivery or validate an SPF policy.
- `dmarc`, `dmarcPolicy`: explicit DMARC TXT records and `none`, `quarantine`,
  `reject` or `null`. Multiple records, duplicate policy tags and invalid policy
  values yield `null`. Parent-domain inheritance is not evaluated.
- `dnssec`: RDAP `delegationSigned`, DS presence and the resolver's AD flag.
  `hasDnssec` is true if RDAP declares a signed delegation or a DS answer exists.
  These are presence signals; they do not constitute an independent DNSSEC audit.
- `dnsRcodes`: resolver response codes by query. `0` is NOERROR and `3` is NXDOMAIN.

Unrequested records and derived flags are `null`; requested records with no answer
are empty arrays and have false presence flags. The RDAP DNSSEC signal can remain
available when DNS checks are disabled. DNS authenticated evidence is true if any
completed query has AD set.

An error row has this form and does not incur a domain event:

```json
{
  "index": 0,
  "domain": "example.invalid",
  "inputDomain": "example.invalid",
  "registrableDomain": "example.invalid",
  "success": false,
  "error": {"code": "NO_RDAP_SERVER", "message": "IANA bootstrap has no HTTPS RDAP service for this TLD"}
}
```

Other error codes include `INVALID_INPUT`, `INVALID_DOMAIN`, `BOOTSTRAP_ERROR`,
`NOT_FOUND`, `INVALID_RDAP`, `RATE_LIMITED`, `HTTP_ERROR`, `NETWORK_ERROR`,
`UNSAFE_URL`, `RESPONSE_TOO_LARGE`, `INVALID_RESPONSE` and `TIMEOUT`.
`DNS_ERROR` is reported through `dnsError` on partial rows.
An input-level error has `index: null` and `domain: null`, and fails the run.
RDAP or DNS errors do not establish that a domain is available for registration.

### Sources and limits

The IANA RDAP bootstrap is downloaded once per run. HTTPS service URLs are tried
in their bootstrap order. RDAP responses are reduced to the documented fields.
Registrant contacts, addresses, emails, telephone numbers, raw entities and raw
RDAP responses are not returned. TXT values are public DNS records and are
returned as requested; an input URL path or its credentials is never echoed.

Cloudflare's JSON DNS API is used first, with Google as fallback. The fallback
also applies to malformed answers, SERVFAIL, refused queries and truncation.
NXDOMAIN is a completed response. A DNS failure after both providers fail keeps
the RDAP result as a billable partial audit. DNS timeout also yields a partial
audit. RDAP failures remain uncharged error rows.

HTTPS requests use a validated public destination IP, preserve TLS SNI and Host,
and disable proxy environment variables. DoH uses HTTP/1.1 keep-alive with a
separate connection pool per resolver, up to eight concurrent requests and a
60-second cache of validated destination addresses. Other servers, including
RDAP, keep one active request per host with no connection reuse. The peer IP is
checked on every response, including reused connections. Each redirect selects
the destination's pool and is validated. Private, reserved, loopback, link-local, multicast and transition
addresses are blocked. Responses are limited to 2 MiB; compression is disabled.
Each request has a 15-second timeout and a 10-second connect timeout. HTTP 429 and
5xx responses receive at most three attempts with exponential backoff.
`Retry-After` seconds and HTTP dates are honored. A delay over 60 seconds ends
the lookup without an early retry.

Each domain has a 60-second processing limit. Its requested DNS queries run in
parallel, within the time left after RDAP. The run has a 270-second processing
budget. Domains which cannot start or complete RDAP receive uncharged timeout
rows if that budget ends. Valid RDAP plus DNS timeout yields a billable partial.
Set the Apify run timeout to 300 seconds. A slow server or a large list can exceed
the processing budget; use shorter lists for such servers. A worker starts the
next domain as soon as it finishes. Output stays in input order; the number of
active and buffered audits is limited to twice `maxConcurrency`. Workers are
cancelled when the charge cap is reached or dataset delivery fails. The Actor logs the
processed row count, charged event count and skipped duplicate count.

### Pricing

US$ 0.003 per successful unique host audit, via `domain-audited`, including partial
DNS results with valid RDAP. RDAP errors and duplicate hosts
do not incur this event. The SDK respects `ACTOR_MAX_TOTAL_CHARGE_USD` and stops
when the charge limit is reached. The result at that limit and subsequent results
can be omitted. See [pricing.md](pricing.md) for Console configuration.

### Local development

From this Actor directory:

```powershell
uv venv --python 3.12 .venv
uv pip install --python .venv/Scripts/python.exe -r requirements-dev.txt
& .venv/Scripts/python.exe -m pytest -q
```

The tests block external HTTP and DNS. They cover mocked bootstrap, RDAP, DoH,
retries, private-IP protection, budgets and local SDK dataset delivery. Set
`storage/key_value_stores/default/INPUT.json` and run `python -m src` to perform a
local lookup with network access. Configure a 300-second timeout when deploying.
Local tests do not verify a Docker build or an Apify platform run.

`python benchmarks/throughput.py --report benchmarks/throughput-1000.json` runs
an offline 1,000-domain benchmark with 200 ms per RDAP/DoH request, the production
HTTPX pools and a mock dataset/charge sink. See [verification.md](verification.md)
for measured latency and process memory; these are local mock results.

Protocol references: [IANA bootstrap](https://data.iana.org/rdap/dns.json),
[RDAP JSON responses, RFC 9083](https://www.rfc-editor.org/rfc/rfc9083.html),
[Cloudflare JSON DoH](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/),
[Google JSON DoH](https://developers.google.com/speed/public-dns/docs/doh/json),
[offline PSL extraction](https://github.com/john-kurkowski/tldextract#how-to-disable-http-suffix-list-fetching-for-production).

# Actor input Schema

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

1–1000 domain names or HTTP(S) URLs. IDNs become punycode. RDAP uses the registrable domain from the offline Public Suffix List; DNS uses the requested host. Paths are removed; credentials and ports are rejected. Equivalent hosts produce one result.

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

Enable DNS-over-HTTPS queries. False disables all DNS, DMARC and DS queries.

## `dnsRecordTypes` (type: `array`):

Records to fetch. Derived MX and SPF fields are null when their record type is not selected.

## `checkDmarc` (type: `boolean`):

Query TXT at \_dmarc.<domain>. No organizational-domain inheritance lookup.

## `checkDnssec` (type: `boolean`):

Query DS records and expose resolver AD validation. This is presence evidence, not an independent cryptographic audit.

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

1–10 workers with ordered output and bounded buffering. One active RDAP request per server; eight concurrent requests per DoH resolver.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "https://iana.org/domains"
  ],
  "checkDns": true,
  "dnsRecordTypes": [
    "A",
    "AAAA",
    "MX",
    "NS",
    "TXT"
  ],
  "checkDmarc": true,
  "checkDnssec": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Registration and DNS results, including uncharged per-domain errors.

# 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",
        "https://iana.org/domains"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kadev-code/domain-rdap-dns").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",
        "https://iana.org/domains",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("kadev-code/domain-rdap-dns").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",
    "https://iana.org/domains"
  ]
}' |
apify call kadev-code/domain-rdap-dns --silent --output-dataset

```

## MCP server setup

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

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/3wv1SKXLgghKbZ9gF/builds/86PnzuTc0h8iKZZZp/openapi.json
