# Bulk Domain Intelligence (`brostdigital/domain-intel`) Actor

Check registration, expiry, DNS, SPF/DMARC and SSL for up to 10,000 domains per run. One clean record per domain, no personal data.

- **URL**: https://apify.com/brostdigital/domain-intel.md
- **Developed by:** [Brost Digital](https://apify.com/brostdigital) (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 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 Domain Intelligence

**Registration, DNS, email security and SSL data for thousands of domains in one run.**

Paste a list of domains (or website addresses, or email addresses) and get one tidy row per domain that answers:

- **Who registered it through, and when does it expire?** Registrar, registration date, expiry date, days left, status codes, DNSSEC, name servers.
- **Where is its email hosted, and is it protected?** MX records and a best guess at the email provider (Google Workspace, Microsoft 365 and others), plus SPF and DMARC with the DMARC policy.
- **Is its SSL certificate healthy?** Issuer, valid-from and valid-to dates, days until expiry, the names it covers, and a plain label when something is wrong (expired, wrong name, self-signed, no HTTPS, and so on).
- **Plus the raw DNS:** A, AAAA, MX, NS, TXT, CAA and SOA records.

All data comes from open, public standards built for automated lookups: **RDAP** (the modern replacement for WHOIS, using the registries' own servers as listed by IANA), **DNS** (through Cloudflare's or Google's public resolver) and a normal **TLS connection** to the website.

### Who it's for

- **Domain investors and portfolio owners:** watch expiry dates and registrars across many domains.
- **IT and security teams:** find SSL certificates about to expire, and domains missing SPF or DMARC or with a weak DMARC policy.
- **Sales and lead research:** see how old a company's domain is and which email provider it uses.
- **SEO and website audits:** check registration, DNS and HTTPS for a list of sites in one go.

### Sample output

The **Overview** view, from a real run on 30 September 2026:

| Domain        | Registrar                        | Registered on | Expires on | Days until expiry | Email provider          | Has DMARC | SSL days left | Status |
| ------------- | -------------------------------- | ------------- | ---------- | ----------------- | ----------------------- | --------- | ------------- | ------ |
| google.com    | MarkMonitor Inc.                 | 1997-09-15    | 2028-09-14 | 714               | Google Workspace        | true      | 63            | ok     |
| apify.com     | Amazon Registrar, Inc.           | 2009-06-02    | 2035-06-02 | 3166              | Google Workspace        | true      | 108           | ok     |
| wikipedia.org | MarkMonitor Inc.                 | 2001-01-13    | 2027-01-13 | 104               | other                   | true      | 33            | ok     |
| bbc.co.uk     | British Broadcasting Corporation | 1994-12-13    | 2034-12-13 | 2995              | Broadcom Email Security | true      | 115           | ok     |
| cira.ca       | CIRA Default Registrar           | 1998-02-05    | 2050-02-05 | 8528              | Microsoft 365           | true      | 61            | ok     |
| github.io     |                                  |               |            |                   | none                    | false     | 31            | ok     |

github.io has no registration data because .io has no RDAP service (see [What it doesn't do](#what-it-doesnt-do)). The dataset also has **Registration**, **DNS and email security**, **SSL certificate** and **Errors and warnings** views, and every field is available in JSON, CSV and Excel exports.

<details>
<summary>One full record (JSON; some long lists shortened)</summary>

```json
{
    "domain": "apify.com",
    "input": "info@apify.com",
    "tld": "com",
    "checkedAt": "2026-09-30T20:40:37.435Z",
    "status": "ok",
    "errors": [],
    "warnings": [],
    "rdapAvailable": true,
    "rdapServer": "https://rdap.verisign.com/com/v1/",
    "registered": true,
    "registrar": "Amazon Registrar, Inc.",
    "registrarIanaId": "468",
    "registrarAbuseEmail": "trustandsafety@support.aws.com",
    "createdAt": "2009-06-02T17:14:10.000Z",
    "updatedAt": "2026-05-16T16:53:04.000Z",
    "expiresAt": "2035-06-02T17:14:10.000Z",
    "domainAgeDays": 6329,
    "daysUntilExpiry": 3166,
    "statusCodes": ["client transfer prohibited"],
    "dnssec": true,
    "nameServers": ["ns-1225.awsdns-25.org", "ns-1928.awsdns-49.co.uk", "ns-449.awsdns-56.com", "ns-839.awsdns-40.net"],
    "a": ["99.86.57.55", "99.86.57.58", "99.86.57.83", "99.86.57.91"],
    "aaaa": ["2600:9000:2132:1a00:9:a03e:6540:93a1", "…"],
    "mx": [
        { "priority": 1, "exchange": "aspmx.l.google.com" },
        { "priority": 5, "exchange": "alt1.aspmx.l.google.com" }
    ],
    "ns": ["ns-1225.awsdns-25.org", "ns-1928.awsdns-49.co.uk", "ns-449.awsdns-56.com", "ns-839.awsdns-40.net"],
    "txt": ["tito-domain-verification=pbg2696rvzbax2t4dl7h5ynqm", "…"],
    "caa": ["0 issue \"amazonaws.com\"", "0 issue \"letsencrypt.org\"", "…"],
    "soa": {
        "nsname": "ns-839.awsdns-40.net",
        "hostmaster": "awsdns-hostmaster.amazon.com",
        "serial": 1,
        "refresh": 7200,
        "retry": 900,
        "expire": 1209600,
        "minttl": 86400
    },
    "spf": "v=spf1 a mx include:_spf.google.com include:mailgun.org include:amazonses.com include:19497222.spf05.hubspotemail.net -all",
    "hasSpf": true,
    "dmarc": "v=DMARC1; p=reject; sp=reject; pct=100; rua=mailto:dmarc-reports@apify.com; ri=604800",
    "hasDmarc": true,
    "dmarcPolicy": "reject",
    "emailProvider": "Google Workspace",
    "sslHost": "apify.com",
    "sslIssuer": "Amazon",
    "sslValidFrom": "2026-07-03T00:00:00.000Z",
    "sslValidTo": "2027-01-16T23:59:59.000Z",
    "sslDaysUntilExpiry": 108,
    "sslNames": ["*.apify.com", "apifier.com", "…"],
    "sslError": null
}
```

</details>

### How to use

1. Open the Actor's **Input** tab.

2. Paste your domains into **Domains**, one per line. You can mix formats:

   - plain domains: `example.com`
   - website addresses: `https://www.example.com/pricing`
   - email addresses: `jane@example.com`

   Each entry is reduced to its registrable domain using the Public Suffix List, so `https://shop.example.co.uk/cart` becomes `example.co.uk`. Duplicates are removed, and entries that aren't domains are reported as error records (free). Up to 10,000 entries per run.

3. Optionally choose which **checks** to run (all three by default) and the other options below.

4. Click **Start**. When the run finishes, open the **Output** tab or export the dataset.

Example input (the same thing you'd send through the API):

```json
{
    "domains": ["apify.com", "https://www.bbc.co.uk/news", "jane@example.org"],
    "checks": ["rdap", "dns", "ssl"],
    "followRegistrarRdap": true,
    "dnsResolver": "cloudflare",
    "maxConcurrency": 20
}
```

| Input                     | What it does                                                                                                                                                                                   | Default    |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| **Domains** (required)    | The domains, website addresses or email addresses to check.                                                                                                                                    | none       |
| **Checks**                | Which data to collect: `rdap` (registration), `dns` (DNS and email security), `ssl` (certificate). Fields for checks you skip are left empty (`null`).                                         | all three  |
| **Follow registrar RDAP** | Some registries (for example .com and .net) keep only basic data and point to the registrar for more. When on, the registrar is asked too. If the two disagree, the registry's answer is used. | on         |
| **DNS resolver**          | `cloudflare` (1.1.1.1) or `google` (8.8.8.8). A fixed public resolver gives consistent results.                                                                                                | cloudflare |
| **Max concurrency**       | How many domains are checked at the same time (1 to 100). Each registry's server also has its own, lower limit, so a large list from one registry is paced to what that registry allows.       | 20         |

### Understanding the output

#### Status: ok, partial or error

Every record has a `status`:

- **ok:** every check you asked for returned an answer.
- **partial:** at least one check returned an answer, but something failed. The record still has everything that worked, and `errors` says what didn't.
- **error:** no check returned an answer, or the input wasn't a domain. Error records are free.

#### Errors versus warnings

- **`errors`** list checks that **failed**, for example the registry didn't answer in time, or one DNS lookup timed out. Any error makes the record `partial` (or `error` if nothing worked).
- **`warnings`** list **extra data that couldn't be fetched** while the main answer is fine. The usual one: the registry answered, but the registrar's server (asked for fuller data) didn't. A warning never changes the status.

Each entry has a `check` (`rdap`, `dns`, `ssl` or `input`), a short `code` and a plain-language `message`.

#### Empty (null) versus false or "none"

- **`null` means we couldn't find out.** Either you didn't ask for that check, or the lookup it depends on failed (and `errors` says which). For example, if the DMARC lookup times out, `hasDmarc` and `dmarcPolicy` are `null`.
- **`false`, `"none"` or an empty list means we checked and it isn't there.** For example, `hasDmarc: false` means the domain really has no DMARC record, and `emailProvider: "none"` means it has no mail servers (or explicitly says it accepts no email).

So `hasDmarc: false` is a finding you can act on, while `hasDmarc: null` means "try again".

A few more fields where the difference matters:

- **`registered`:** `false` means the registry says the domain isn't registered. `null` means unknown: there's no RDAP service for that TLD, or the lookup failed.
- **`rdapAvailable`:** `false` means the domain's TLD has no RDAP service, so there's no registration data (not an error).
- **`emailProvider`:** a best guess from the mail servers. `other` means the servers are in use but aren't a provider we recognize (often a company's own mail servers).
- **`daysUntilExpiry`** and **`sslDaysUntilExpiry`:** negative numbers mean it has already expired.

#### SSL problems (`sslError`)

The SSL check connects to the domain itself first and, only if that doesn't answer on the HTTPS port, to `www.` + the domain. `sslHost` tells you which one was checked. These are findings about the website, not failures of the check, so the record's status stays `ok`.

| `sslError`              | What it means                                                                                                                                         |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| *(empty)*               | The certificate is fine: in date, covers the host name, and is issued by a trusted authority.                                                         |
| `expired`               | The certificate's end date has passed.                                                                                                                |
| `not-yet-valid`         | The certificate's start date is in the future.                                                                                                        |
| `hostname-mismatch`     | The certificate is for a different name than the host we connected to.                                                                                |
| `self-signed`           | The website signed its own certificate instead of getting one from a trusted authority.                                                               |
| `untrusted`             | The certificate chain doesn't lead to an authority that browsers trust.                                                                               |
| `no-https`              | Neither the domain nor its `www.` address accepts HTTPS connections (or has no address at all).                                                       |
| `timeout`               | The server didn't complete the connection within 8 seconds, so we couldn't read a certificate.                                                        |
| `tls-error`             | The server answered but the secure connection failed for another reason.                                                                              |
| `ipv6-only-not-checked` | The host only has an IPv6 address; this version checks SSL over IPv4 only, so the certificate wasn't checked. It does **not** mean there is no HTTPS. |

### Pricing

**$3 per 1,000 domains checked** ($0.003 per domain), charged per domain as its result is saved.

- You pay only for domains where **at least one check returned an answer** (status `ok` or `partial`).
- **Free:** entries that aren't valid domains, duplicates, and domains where every check failed (status `error`).
- You're never charged twice for the same domain in a run, even if the platform restarts it.
- If you set a **spending limit** for the run, the Actor stops cleanly when it's reached and the status message says how many domains weren't checked.

### Personal data

This Actor **never outputs personal data about domain owners.** Registries sometimes return details of the registrant (the owner), and of administrative, technical or billing contacts, sometimes without redacting them. The Actor ignores all of them. The only contact data in the output is the **registrar's** (the company the domain was registered through): its name, IANA ID and abuse email address, which are published business contacts.

### What it doesn't do

- **No owner or contact details.** See [Personal data](#personal-data).
- **Some country-code TLDs have no registration data.** Registration data comes from RDAP, and not every registry offers RDAP yet. When a TLD isn't in IANA's list of RDAP services, the record has `rdapAvailable: false` and empty registration fields; the DNS and SSL checks still run. In our test this included **.de, .io, .jp, .eu, .ch, .it, .se, .es, .us and .edu**. The old WHOIS system isn't used.
- **Some registries limit how many lookups they answer.** When a registry asks us to wait longer than a couple of minutes, the Actor respects that and stops asking it for the rest of the run; those domains come back `partial` with the error `rdap-rate-limited` (DNS and SSL data are still there). In our test, the .au registry answered about 20 lookups and then asked us to wait a day.
- **SSL over IPv6 isn't checked.** Hosts with only an IPv6 address get `ipv6-only-not-checked`.
- **Subdomains are reduced to the registrable domain.** `blog.example.com` is checked as `example.com`, for registration, DNS and SSL alike.
- **It doesn't watch domains by itself.** Each run is a snapshot; for ongoing monitoring, schedule it (see below).

### FAQ

**How do I monitor domains over time?**
Save your input as a task and add a schedule (for example, weekly) in Apify Console. Each run produces a fresh dataset you can compare, export, or send to a spreadsheet or webhook through Apify integrations. Useful fields to watch: `daysUntilExpiry`, `sslDaysUntilExpiry`, `sslError`, `hasDmarc` and `dmarcPolicy`.

**Can I use it from my own code or an AI agent?**
Yes. Like any Apify Actor it can be started through the Apify API or the official API clients, with the same JSON input as above. Apify's MCP server can also make it available to AI agents. Results come back as a dataset in JSON, CSV or Excel.

**How fast is it?**
In our test of 1,000 mixed domains (about 40% .com, plus .org, .net and more than a dozen country codes), the run took about 2.5 minutes on Apify, roughly 390 domains per minute, with all three checks on. Registration lookups set the pace: to stay within what registry servers allow, the Actor sends at most two requests at a time to each registry and backs off when a registry asks it to, so a very large list from a single TLD runs slower than a mixed one. The 10,000-entry limit keeps a run comfortably inside Apify's default one-hour run timeout.

**What happens if the run is restarted or migrated?**
Progress is saved as the run goes. If the Apify platform restarts or moves the run, it continues where it left off: domains that already have a result aren't checked again, don't appear twice in the dataset, and aren't charged twice.

**Why is the registrar data sometimes missing for a registered domain?**
Either the TLD has no RDAP service (`rdapAvailable: false`), or the registry didn't answer or asked us to slow down (see `errors`). For .com and .net, basic data comes from the registry and fuller data from the registrar; if only the registrar fails, you still get the registry's data plus a warning.

**Why does a domain show `emailProvider: "other"`?**
Its mail servers aren't operated by a provider we recognize, often because the organization runs its own. The raw MX records are in `mx`.

**What does the final status message mean?**
At the end of each run you'll see a summary like "9,812 of 10,000 domains fully checked; 176 partial; 12 errors (see error records)". Filter the dataset by `status`, or open the **Errors and warnings** view, to see which domains need another look.

# Actor input Schema

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

Domains to check, one per line. You can paste URLs (https://www.example.com/page) or email addresses (name@example.com); each is reduced to its registrable domain (example.com, example.co.uk) and duplicates are removed. Up to 10,000 entries per run. Invalid entries are reported as error records.

## `checks` (type: `array`):

Which data to collect. RDAP: registrar, dates, status, name servers. DNS: records plus SPF, DMARC and email provider. SSL: certificate issuer, validity and names.

## `followRegistrarRdap` (type: `boolean`):

For registries that return thin data (e.g. .com and .net), also query the registrar's RDAP server for fuller data. Registry values win when the two disagree.

## `dnsResolver` (type: `string`):

Public resolver used for DNS lookups and to find the address for the SSL check. A fixed resolver gives consistent results.

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

How many domains are checked at the same time. Each registry's RDAP server has its own lower limit on top of this, so large runs of one TLD are paced to what the registry allows.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "example.org"
  ],
  "checks": [
    "rdap",
    "dns",
    "ssl"
  ],
  "followRegistrarRdap": true,
  "dnsResolver": "cloudflare",
  "maxConcurrency": 20
}
```

# Actor output Schema

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

No description

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

No description

# 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",
        "example.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("brostdigital/domain-intel").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",
        "example.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("brostdigital/domain-intel").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",
    "example.org"
  ]
}' |
apify call brostdigital/domain-intel --silent --output-dataset

```

## MCP server setup

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

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/HVeo55Ef3vjaJn5t2/builds/nIkHpRZUS58MkElP0/openapi.json
