# Domain Intelligence & Enrichment — WHOIS, DNS, Email Provider (`brenton8907/domain-intel-check`) Actor

Bulk domain enrichment and DNS check, one row per domain or email: email provider (Google Workspace, M365, Proofpoint), DMARC/SPF/DKIM, WHOIS/RDAP domain age and registrar, SSL type (EV/OV/DV), hosting ASN (AWS, Cloudflare), CDN, tech stack and a maturity score.

- **URL**: https://apify.com/brenton8907/domain-intel-check.md
- **Developed by:** [Brenton Keller](https://apify.com/brenton8907) (community)
- **Categories:** Lead generation, SEO tools, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 domain enricheds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Intelligence Check

What does a company run, and how seriously does it run it?

Bulk domain enrichment: a WHOIS lookup, a DNS check and an email-provider check in one row. Input
domains, URLs or email addresses and get one row for each:

- who handles the company's mail (Google Workspace, Microsoft 365, or a security gateway such as
  Proofpoint)
- whether its domain is protected against spoofing (SPF, DMARC, DKIM)
- how old the domain is and who registered it
- what kind of TLS certificate it serves
- what sits in front of its website, and which network hosts it (AWS, Google Cloud,
  Cloudflare, or the company's own network)
- which SaaS tools it has verified ownership with

A **maturity score** adds those up, and **`maturity_signals`** gives the reasons for the score.

```
stripe.com   92  registered 31 years ago · Google Workspace · DMARC p=reject · EV certificate
accenture.com 100 registered 26 years ago · mail behind Proofpoint · DMARC p=reject · OV cert · CloudFront
notion.so    70  registered 11.5 years ago · DMARC p=quarantine · behind Cloudflare · no MX on this domain
```

Everything comes from public infrastructure: DNS (including an IP-to-ASN lookup), RDAP/WHOIS, a
TLS handshake and one request to the homepage. That is why it is fast. **14 inputs took 1.5 s** and there's no proxy to pay for.

### Paste a column of emails

A lead list is usually a column of email addresses, so the actor accepts them directly:

- `jane@figma.com` → `figma.com`
- `https://shop.acme.co.uk/x` → `acme.co.uk`

Each line you submit gets its own row, with your original text in `query`, so the results join
straight back onto your sheet.

Free mailbox domains (gmail.com, outlook.com, yahoo.com …) say nothing about a person's
employer. They are returned with a note and **not charged**.

### Output

| Field | Example | Notes |
|---|---|---|
| `maturity_score` | `92` | 0–100. The weights are below. |
| `maturity_signals` | `["domain registered 31.1 years ago", "uses google workspace", "DMARC p=reject enforced", "EV certificate (organisation validated)"]` | |
| `email_provider` | `google_workspace`, `microsoft_365`, `proofpoint`, `self_hosted`, `no_mx` … | Read from MX records. |
| `email_provider_kind` | `mailbox` · `gateway` · `forwarder` · `self_hosted` · `none` · `unknown` | |
| `email_security` | `enforced` · `partial` · `monitoring` · `spf_only` · `none` | The DMARC/SPF posture in one word. |
| `dmarc_policy` / `dmarc_pct` | `reject` / `100` | |
| `spf_all` | `fail` (`-all`), `softfail` (`~all`) … | |
| `spf_senders` | `["google", "hubspot", "zendesk", "postmark"]` | Services allowed to send mail as the domain. |
| `dkim_selectors_found` | `["google", "s1", "s2"]` | |
| `mta_sts` | `enforce` · `testing` · `none` · `policy_unreachable` · `policy_invalid` · `absent` | Whether inbound mail must use TLS. Read from the published policy file, not just the DNS record, and checked against RFC 8461: the version, a mode, `max_age`, and at least one `mx` unless the mode is `none`. When the DNS record exists but the policy can't be used, mail servers ignore it. `policy_unreachable` means the policy file couldn't be fetched; `policy_invalid` means it was fetched but is incomplete. |
| `tls_rpt` | `true` | Publishes TLS failure reporting. |
| `bimi` / `bimi_vmc` | `true` / `true` | Brand logo in the inbox. `bimi_vmc` means a Verified Mark Certificate, a paid trademark check (paypal.com, cnn.com). |
| `txt_verified_services` | `["atlassian", "docusign", "microsoft_365", "slack", "stripe"]` | SaaS tools whose domain-verification record is published. |
| `registered_on` / `domain_age_years` | `1995-09-12` / `31.1` | The age of the *domain*, not the company. A bought domain carries its original date: cal.com reads 1997 and vercel.com 1999. |
| `registrar` / `registrant_org` | `MarkMonitor Inc.` / `British Broadcasting Corporation` | The organisation only, and only when it is published. |
| `tls_valid` / `tls_cert_type` | `true` / `EV` | The type comes from the certificate's CA/Browser Forum policy ID, not a guess from the issuer name. |
| `tls_issuer` / `tls_subject_org` / `tls_days_to_expiry` | `DigiCert, Inc.` / `Stripe, LLC` / `64` | |
| `cdn` / `cdn_source` / `hosting_platform` | `fastly` / `asn` / `shopify` | The CDN is read from response headers. If the headers name none, it falls back to the hosting network when that network is a CDN (nytimes.com sends no Fastly header but sits on Fastly). `cdn_source` says which. AWS addresses are never assumed to be CloudFront, because the same network also carries plain servers. |
| `web_host` | `www.toyota.co.jp` | The host the website checks ran on. It falls back to `www.` when the bare domain has no address. |
| `redirects_to_https` / `hsts_max_age` / `security_headers_present` | `true` / `63072000` / `[...]` | |
| `hosting_provider` / `hosting_asn` / `hosting_asn_org` | `aws` / `16509` / `Amazon.com, Inc.` | The network serving the domain's IP address. `own_network` means the company announces its own address space (accenture.com on AS3573), which few companies do. |
| `dns_provider` / `dnssec` / `has_ipv6` / `caa_issuers` | `aws_route53` / `true` / `true` / `["digicert.com"]` | |
| `hint` | | Set whenever an answer needs a caveat. Read it. |
| `charged` | `true` | Whether this row was billed. |

Raw records (`mx_hosts`, `spf_record`, `dmarc_record`, `nameservers`) are included, so you
can check any classification yourself.

### Read these before trusting a column

**`email_provider` comes from MX, never from SPF.** SPF lists who may *send* mail as the
domain, which includes every marketing and transactional tool, not who *receives* it. A test
version that read SPF reported Amazon SES as Pfizer's mail provider. SPF goes into
`spf_senders` instead, where it's accurate and useful.

**A security gateway is reported as the gateway.** When MX points at Proofpoint, Mimecast or
Cisco, the mailbox behind it is hidden. We return `proofpoint` / `gateway` rather than a guess.
"Uses Proofpoint" is a strong signal in its own right: it implies an enterprise security budget.

**`no_mx` is an answer, not an error.** About 28% of a real prospect list has no MX record,
and most of those domains still serve a website. Companies often split their web and mail
domains: notion.so has no MX, and Notion's mail runs on makenotion.com.

**No DKIM found isn't proof there's no DKIM.** DKIM keys sit under selector names that can't be
listed from DNS. The actor tries 12 common selectors and reports how many it checked. Add your
own in `extraDkimSelectors`.

**`hosting_provider` is whoever serves the address, not the origin behind it.** With a CDN in
front, it's the CDN: paypal.com shows `fastly`. It's read from the IPv4 address of the bare
domain, so a site that serves only from `www.` may show a different network there. When the
address has no BGP origin, or the ASN service returns something unexpected, the hosting
columns are left empty and `hint` says which happened.

**`txt_verified_services` means "verified at some point".** A verification token can outlive
the subscription.

### Maturity score

| Component | Points |
|---|---|
| Domain age ≥10 years / ≥5 / ≥2 | 20 / 15 / 8 |
| Mail behind a security gateway / on Google Workspace or M365 / any other real mailbox | 15 / 12 / 6 |
| DMARC enforced (quarantine or reject at 100%) / partial / monitoring only | 15 / 10 / 5 |
| SPF ends in `-all` or `~all` | 5 |
| DKIM key found | 5 |
| Valid TLS certificate | 10 |
| … that is OV or EV (organisation validated) | +10 |
| HTTP redirects to HTTPS with HSTS / without HSTS | 10 / 5 |
| Behind a CDN (by headers or hosting network) | 5 |
| 3 or more security headers | 5 |

MTA-STS and BIMI appear in `maturity_signals` but carry no points, so scores stay comparable
with earlier runs.

If a component couldn't be measured (website down, registry publishes no dates), it scores 0
and is listed in `maturity_unmeasured`. A low score caused by missing data is visible as such.

### Pricing

| Event | Price | When |
|---|---|---|
| `domain-intel` | $0.003 | One domain enriched. |

**Not charged:**

- invalid input: a company name, a public suffix such as `co.uk`, an IP address, or a malformed
  domain such as `stripe..com`. Typos are rejected with the reason, never "corrected" into
  someone else's domain.
- free mailbox domains
- domains that don't resolve
- any row that ends in an error, including a DNS lookup that every resolver refused or failed.
  That is reported as an error, never as "no MX" or "no DMARC"

Every row says whether it was charged in `charged`.

### Coverage

- **Registration data** comes from RDAP (the IANA bootstrap list) and falls back to WHOIS for
  TLDs without RDAP. In testing that included .io, .co, .so and .jp. Older .co.jp names publish
  the registrant organisation but no creation date.
- Some registries publish no creation date at all (DENIC for .de, for example). Those rows have
  a null age and say so.
- The registrant organisation is often redacted under GDPR. It is shown only when a real
  organisation is published. Privacy-service placeholders and personal names, emails and
  phone numbers are never returned.

### Limitations

- Email-provider detection matches known MX hostnames. A provider that doesn't appear in the
  list is reported as `other`, with the raw `mx_hosts` alongside so you can classify it yourself.

# Actor input Schema

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

One per line. Email addresses are reduced to their domain (jane@acme.com -> acme.com) and URLs to their registrable domain (https://shop.acme.co.uk/x -> acme.co.uk). Each line gets its own output row, so results join back to your list. Free mailbox domains (gmail.com, outlook.com ...) are returned flagged and not charged.

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

All are on by default and the price is the same either way. Dropping checks only makes a run faster; the maturity score lists any component it could not measure.

## `extraDkimSelectors` (type: `array`):

DKIM selectors cannot be listed from DNS, so 12 common ones are probed (google, selector1, selector2, k1, s1, s2 ...). Add any your list is known to use.

## `concurrency` (type: `integer`):

Each domain makes about 20 DNS-over-HTTPS queries and 3-5 other requests. 10 in parallel keeps well inside public resolver limits.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "accenture.com",
    "notion.so",
    "jane@figma.com"
  ],
  "checks": [
    "email",
    "dns",
    "registration",
    "tls",
    "http"
  ],
  "extraDkimSelectors": [],
  "concurrency": 10
}
```

# Actor output Schema

## `items` (type: `string`):

One row per submitted domain, URL or email address.

## `csv` (type: `string`):

The same rows as CSV, for spreadsheets.

## `run` (type: `string`):

The run page with the Overview table, status message and log.

# 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",
        "accenture.com",
        "notion.so",
        "jane@figma.com"
    ],
    "extraDkimSelectors": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("brenton8907/domain-intel-check").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",
        "accenture.com",
        "notion.so",
        "jane@figma.com",
    ],
    "extraDkimSelectors": [],
}

# Run the Actor and wait for it to finish
run = client.actor("brenton8907/domain-intel-check").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",
    "accenture.com",
    "notion.so",
    "jane@figma.com"
  ],
  "extraDkimSelectors": []
}' |
apify call brenton8907/domain-intel-check --silent --output-dataset

```

## MCP server setup

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

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/hgOvIWbNnrDVBaDTH/builds/4eHl4RjYi3raorWnx/openapi.json
