# Domain Checker: Email Provider, SPF/DMARC, SSL & Expiry (`watchmere/domain-checker`) Actor

One row per company domain: email provider (Microsoft 365, Google Workspace...), SPF, DMARC and DKIM with an A-F grade, DNS host, HTTPS and SSL expiry, registrar and expiry date via RDAP, plus what changed since your last check.

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

## Pricing

from $4.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

### What does Domain Checker do?

Give Domain Checker a list of company websites, domains or email addresses and get **one row per company** that
shows how its email and website are set up:

- **Email provider**: Microsoft 365, Google Workspace, Zoho, Proofpoint, Mimecast, Barracuda, Amazon SES/WorkMail,
  GoDaddy, Yahoo, Fastmail, ProtonMail, self-hosted and more, read from the MX records. When mail goes through a
  filtering service such as Proofpoint, the SPF record often reveals the mailbox provider behind it.
- **Email security**: SPF (policy, services allowed to send, DNS lookup count, and whether the record is broken: an
  include with no SPF record, more than 10 lookups, and the other "permanent errors" mail servers reject), DMARC (policy, subdomain policy, percentage, whether reports are collected), DKIM keys on common selectors, MTA-STS
  and BIMI, summed up as an **A–F grade with a one-line reason**.
- **DNS**: name servers and DNS host (Cloudflare, AWS Route 53, GoDaddy, Azure, Google Cloud DNS, Namecheap...), and
  whether the domain has IPv4/IPv6 addresses.
- **Website**: does it answer over HTTPS, does HTTP redirect to HTTPS, final URL after redirects, status code and
  server header.
- **SSL certificate**: issuer, expiry date, days left, whether browsers trust it and whether it matches the name.
- **Registration**: registrar, created, updated and expiry dates, days to expiry, status codes and DNSSEC, from
  **RDAP**, the official successor to WHOIS run by each registry.
- **What changed since your last check**: provider switched, DMARC tightened, certificate or domain renewed,
  registrar transfer, new name servers... Schedule it weekly and you have a monitor.

Paste `https://www.acme.co.uk/contact`, `jane@acme.co.uk` or `acme.co.uk`: they all become `acme.co.uk` and are
checked once. Hosted subdomains count as their main domain: `foo.github.io` is checked as `github.io` and
`shop.myshopify.com` as `myshopify.com`.

### Who is it for?

- **Agencies and MSPs (IT providers):** find prospects on Google Workspace vs Microsoft 365, with no DMARC, with a
  broken SPF record, or with a certificate or domain about to expire. Audit every client domain in one run.
- **Sales teams:** enrich a lead list with email provider, DNS host and email security grade before outreach.
- **Security and IT teams:** watch your own domains and suppliers' domains for changes and expiries.
- **Domain investors and brand managers:** registrar, dates and status for a whole portfolio.

### How to use it

1. Paste your **domains, website addresses or email addresses**, one per line (or upload a list through the API).
2. Leave **Show what changed since the last check** on if you want the `changes` column.
3. Click **Start**. Download the results as CSV, Excel or JSON from the Output tab, or use one of the ready-made views:
   *Overview*, *Email security*, *Website and SSL*, *Registration and DNS* and *Changes since last check*.

### Input example

```json
{
  "domains": ["apify.com", "https://www.shopify.com/pricing", "someone@microsoft.com"],
  "compareWithPrevious": true
}
```

### Output example

One row per domain (shortened; every field is listed in the table below):

```json
{
  "domain": "google.com",
  "input": "google.com",
  "resolves": true,
  "registered": true,
  "emailProvider": "Google Workspace",
  "mailboxProvider": "Google Workspace",
  "mxHosts": ["smtp.google.com"],
  "emailSecurityGrade": "A",
  "emailSecuritySummary": "A: SPF soft-fails other senders (~all); DMARC rejects spoofed mail; no DKIM key on common selectors",
  "spfPolicy": "~all",
  "spfLookupCount": 1,
  "spfServices": ["Google Workspace"],
  "dmarcPolicy": "reject",
  "dmarcReporting": true,
  "dkim": "Not found on the 10 common selectors checked (DKIM may still use a custom selector)",
  "mtaSts": true,
  "dnsProvider": "Google",
  "httpsReachable": true,
  "httpRedirectsToHttps": true,
  "finalUrl": "https://www.google.com/?gws_rd=ssl",
  "statusCode": 200,
  "sslIssuer": "Google Trust Services",
  "sslValidTo": "2026-12-21",
  "sslDaysLeft": 64,
  "sslValid": true,
  "registrar": "MarkMonitor Inc.",
  "domainCreated": "1997-09-15",
  "domainExpires": "2028-09-14",
  "daysToExpiry": 697,
  "changes": [],
  "previousCheckAt": null,
  "checkedAt": "2026-10-18T00:44:12Z",
  "errors": []
}
```

| Field | Meaning |
|---|---|
| domain / input | The registrable domain that was checked, and the first thing you typed that led to it |
| resolves | The domain exists in DNS (false for typos and dead domains) |
| registered | The registry knows the domain (from RDAP); null if the registry couldn't be asked |
| emailProvider | Who receives the domain's mail, from its MX records. "None" = no mail server; "Self-hosted (AWS)" and similar = the company's own mail server on a cloud machine; forwarding services (Namecheap Email Forwarding, Cloudflare Email Routing...) are named as such |
| mailboxProvider | Who hosts the mailboxes: the same as emailProvider, or, behind a filtering service like Proofpoint or Mimecast, the provider named in the SPF record. Null if unclear, and for forwarding services (the real inbox can't be seen) |
| mxHosts | Mail servers, most preferred first |
| emailSecurityGrade / emailSecuritySummary | A–F grade and the reason in plain words (rules below) |
| spfRecord / spfPolicy | The SPF record and what it does with other senders: -all, ~all, ?all, +all, "no all", or missing |
| spfIncludes / spfServices | Domains the SPF record includes, and the services they belong to (Microsoft 365, SendGrid, HubSpot, Mailchimp...) |
| spfLookupCount / spfTooManyLookups | DNS lookups an SPF check needs, counted through every include; above 10, SPF fails |
| spfError | Why the SPF record is broken, if it is: mail servers then treat SPF as a permanent error. For example an include whose target has no SPF record or isn't a valid name, more than 10 lookups, more than 2 lookups that find nothing, or an unknown term. "temperror: X didn't answer" means a name in the record didn't answer on two checks in a row (the first time, the SPF check is just marked as failed) |
| spfRecordCount | Number of SPF records (more than one is an error) |
| dmarcRecord / dmarcPolicy / dmarcSubdomainPolicy / dmarcPct | The DMARC record, p=, sp= and pct= |
| dmarcReporting / dmarcValid | Whether DMARC reports are collected (rua=) and whether the record is valid |
| dkimSelectorsFound / dkim | DKIM keys found on common selectors, and a plain note |
| mtaSts / bimi | MTA-STS and BIMI records present |
| dnsProvider / nameServers | DNS host and name servers |
| hasIPv4 / hasIPv6 | The domain itself has A / AAAA records |
| httpsReachable / httpRedirectsToHttps / finalUrl / statusCode / server | Website check (domain first, then www). httpsReachable means the site answers over HTTPS (false only when HTTPS is refused or the handshake fails; null when it didn't answer in time); whether browsers trust its certificate is in sslValid. httpRedirectsToHttps is null when the site blocked the check (403, 429 or a server error) |
| sslHost / sslIssuer / sslValidTo / sslDaysLeft | Certificate on the website's host |
| sslValid / sslMatchesHostname / sslProblem | Trusted the way browsers judge it, matches the name, and the problem in plain words if not |
| sslChainComplete | false when the server forgets to send its intermediate certificate: browsers download it themselves (and so does this check, so sslValid can still be true), but some apps and older devices fail. If that download fails, sslValid is null for this check |
| registrar / registrarIanaId | Registrar company and its IANA number |
| domainCreated / domainUpdated / domainExpires / daysToExpiry | Registration dates (YYYY-MM-DD) |
| domainStatus / dnssec | Registry status codes (e.g. client transfer prohibited) and whether DNSSEC is on |
| changes / previousCheckAt | What changed since you last checked this domain, and when that was |
| checkedAt | When this check ran (UTC) |
| errors | Checks that couldn't be completed, in plain words. The rest of the row is still filled |

### How the email security grade works

The grade looks at SPF and DMARC, the two records that stop other people sending email as your domain:

| | DMARC p=reject (100%) | DMARC quarantine, or reject on part of the mail | DMARC p=none | No valid DMARC |
|---|---|---|---|---|
| **SPF good** (-all or ~all, and not broken) | **A** | **B** | **C** | **D** |
| **SPF weak** (?all, no "all", or broken: see spfError) | **B** | **C** | **D** | **D** |
| **No SPF** | **C** | **C** | **D** | **F** |

An SPF record with **+all** (anyone may send) is always **F**. DKIM is reported but doesn't change the grade: DKIM
keys can only be found by guessing their name (the selector), so "not found on common selectors" doesn't prove DKIM
is off. Add your own selectors under *Advanced* if you know them.

### Change tracking

With **Show what changed since the last check** on (the default), the Actor keeps a small snapshot of each domain in
a key-value store called `domain-checker-history` in your own Apify account. The next time you check the same domain,
`changes` lists what's different, for example:

- `Email provider changed: Google Workspace → Microsoft 365`
- `DMARC policy changed: none → reject`
- `SSL certificate on www.acme.com renewed: valid to 2026-12-01 → 2027-03-01`
- `Domain renewed: expiry 2027-01-01 → 2028-01-01`
- `Registrar changed: GoDaddy.com, LLC → Cloudflare, Inc.`

The first check of a domain has an empty `changes` list and no `previousCheckAt`. If a check fails one time (a DNS
lookup that times out anywhere inside it, a slow registry, a website that's down), it's listed in `errors`, its old
values are kept and nothing is reported as changed. DKIM keys are compared only for selectors checked both times,
and certificates only for the same host (domain or www). Delete the `domain-checker-history` store to start over.

### How much does it cost?

You pay a fixed price per domain checked; see the **Pricing** tab. Every distinct domain gives one row and one
charge, including domains that turn out not to exist (the row says so). Lines that aren't domains at all are skipped
and not charged, and repeats of the same domain are checked once.

**Checks that fail on our side aren't charged.** If a domain's main DNS lookups (name servers, MX, TXT or DMARC)
time out or the DNS servers don't answer, it's tried again at the end of the run, more slowly. If it still fails,
it isn't saved and isn't charged: it's listed in the `FAILED_DOMAINS` record (with the reason) and in the run's
status message, so you can run those domains again later. A clear answer such as "this domain doesn't exist" is a
real result and is charged as normal. Each run also writes a `SUMMARY` record with the counts.

Runs are quick: in our tests, 50 well-known company domains took 20 to 40 seconds (10 domains are checked at a
time; websites that don't answer take longest).

Set a **maximum cost per run** in the run options and the Actor stops cleanly when it gets there. If a run is
restarted (for example when Apify moves it to another server), it carries on where it stopped instead of checking
and charging for the same domains again.

### Good to know

- **Only public, technical data:** public DNS records, the public website's first response and certificate, and
  registry RDAP data. Registrant names, emails, phone numbers and other contact details are never read or saved. You are
  responsible for how you use the results.
- **RDAP isn't available for every domain ending.** In our tests, .de, .it, .es, .jp, .nz, .eu, .me, .so and .edu
  had no RDAP service in IANA's official list, so those rows have no registration data and `errors` says why (all the
  other checks still run). Some registries share less: .nl and .com.au gave the registrar but no expiry date.
  Registries that limit requests are asked politely (a few at a time, slowing down when they say so); if one still
  refuses, the row is saved without registration data.
- **Websites that block automated visitors** may answer with 403 or a challenge page; the row still shows that the
  site answered, with that status code.
- **DKIM** is checked on 10 common selectors plus any you add, so "not found" isn't proof that DKIM is off.

**Something isn't working or you'd like a feature?** Open an issue on the Issues tab.

# Actor input Schema

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

One per line. Paste bare domains (acme.com), website addresses (https://www.acme.co.uk/contact) or email addresses (jane@acme.com): each is turned into its main domain, and repeats are checked only once.

## `compareWithPrevious` (type: `boolean`):

Keeps a small snapshot of each domain in a key-value store called domain-checker-history in your Apify account, and lists what changed since you last checked it: email provider switched, DMARC policy tightened, certificate renewed, domain renewed or transferred, and more. Schedule the Actor weekly to monitor a list.

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

DKIM keys can only be found by guessing their name (the selector). The common ones (selector1, selector2, google, default, k1, s1, s2, dkim, mail, smtp) are always checked; add your own here, up to 20.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "https://www.shopify.com/pricing",
    "someone@microsoft.com"
  ],
  "compareWithPrevious": true,
  "extraDkimSelectors": []
}
```

# Actor output Schema

## `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",
        "https://www.shopify.com/pricing",
        "someone@microsoft.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("watchmere/domain-checker").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",
        "https://www.shopify.com/pricing",
        "someone@microsoft.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("watchmere/domain-checker").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",
    "https://www.shopify.com/pricing",
    "someone@microsoft.com"
  ]
}' |
apify call watchmere/domain-checker --silent --output-dataset

```

## MCP server setup

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

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/tp9FgFAU3RreVXg3e/builds/38knfseira5VYvIGq/openapi.json
