# Domain Security & Email Deliverability Checker (`isnard/domain-security-checker`) Actor

Check SPF, DMARC, DKIM, MX, MTA-STS, BIMI, CAA, DNSSEC, TLS certificate and security headers for a list of domains. Returns a 0-100 score and plain-English fixes.

- **URL**: https://apify.com/isnard/domain-security-checker.md
- **Developed by:** [Matias Isnard](https://apify.com/isnard) (community)
- **Categories:** Developer tools, Lead generation
- **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

## Domain Security & Email Deliverability Checker

Give it a list of domains. For each one you get the email authentication setup (MX, SPF, DMARC, DKIM, MTA-STS, BIMI), DNS hardening (CAA, DNSSEC), the HTTPS setup of the homepage (certificate, TLS version, redirect, security headers), a 0-100 score and a short list of fixes in plain English.

All checks are passive and public: DNS queries plus one HTTPS GET and one HTTP GET of the homepage. No port scanning, no crawling, no login, no exploitation.

### Who it is for

- **Sales and marketing teams**: check a lead list before outreach, or use missing DMARC/SPF as a conversation opener if you sell email or security services.
- **MSPs and IT providers**: audit all client domains in one run and track the score over time.
- **Security consultants**: get a quick, repeatable baseline before a deeper engagement.
- **Email senders**: confirm your own sending domains are set up correctly before a campaign.

### What it checks

| Area | Check | Notes |
| --- | --- | --- |
| Email | MX records | Sorted by priority. |
| Email | SPF | Record, final qualifier (`-all`, `~all`, `?all`, `+all`), includes, total DNS lookups including nested includes (limit is 10). Multiple SPF records are flagged. |
| Email | DMARC | Record at `_dmarc.<domain>`, policy (`none`, `quarantine`, `reject`), subdomain policy, `pct`, whether `rua` reporting is set. |
| Email | DKIM | **Best effort.** DKIM selectors cannot be listed from DNS, so only common names are tried: `google`, `selector1`, `selector2`, `default`, `k1`, `s1`, `s2`, `mail`. "Not found" does not mean DKIM is off. |
| Email | MTA-STS | `v=STSv1` TXT record at `_mta-sts.<domain>`. The policy file is not fetched. |
| Email | BIMI | `v=BIMI1` TXT record at `default._bimi.<domain>`. |
| DNS | CAA | All CAA records. |
| DNS | DNSSEC | Signal only: a DS record exists at the parent zone. The signature chain is not validated. |
| Web | Certificate | Valid and trusted, issuer, expiry date, days to expiry. |
| Web | TLS | Negotiated version (TLS 1.2 or 1.3). |
| Web | Redirect | Whether `http://<domain>/` answers with a redirect to an `https://` URL. |
| Web | Security headers | `Strict-Transport-Security`, `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, read from the final homepage response after following up to 5 redirects. |

DNS queries go to public resolvers (1.1.1.1 and 8.8.8.8) so results match what mail servers on the internet see.

### Score rubric

Each check is pass or fail. The score is points earned divided by points possible, times 100. Every result includes a `breakdown` array with each check and its points, so you can see exactly where the number comes from.

| Check | Points |
| --- | --- |
| MX records present | 5 |
| Exactly one SPF record | 10 |
| SPF ends in `-all` or `~all` | 5 |
| SPF needs 10 DNS lookups or fewer | 5 |
| DMARC record present and valid | 10 |
| DMARC policy is `quarantine` or `reject` | 10 |
| DMARC `rua` reporting address set | 5 |
| DKIM key found on a common selector (best effort) | 5 |
| MTA-STS record present | 3 |
| BIMI record present | 2 |
| CAA records present | 5 |
| DNSSEC (DS record) present | 5 |
| HTTPS works with a valid certificate | 10 |
| Certificate valid for 14 more days or longer | 2 |
| TLS 1.3 negotiated | 1 |
| HTTP redirects to HTTPS | 5 |
| Each of the 6 security headers present | 2 each, 12 total |
| **Total** | **100** |

Email checks are worth 60 points, DNS 10, web 30. When HTTPS checks are turned off, the score is computed from the 70 DNS and email points and scaled to 0-100. A domain that does not exist scores 0.

The score is a checklist, not a risk rating. A domain that does not send email can still be perfectly safe with a low email score (although publishing `v=spf1 -all` and `p=reject` for such domains is good practice).

### Input

```json
{
    "domains": ["google.com", "https://www.github.com/", "mercadolibre.com.ar"],
    "timeoutSecs": 10,
    "includeHttpChecks": true
}
```

- `domains` (required): domains or URLs. URLs are reduced to the hostname and a leading `www.` is removed. Duplicates are checked once. Invalid entries are skipped with a warning in the log and are not charged.
- `timeoutSecs` (default 10): time limit for each DNS query and each HTTP request.
- `includeHttpChecks` (default true): set to false for DNS-only results.

### Output

One dataset item per domain. Shortened example (full real output for 5 domains is in `ejemplos/salida.json` in the source):

```json
{
    "input": "google.com",
    "domain": "google.com",
    "domain_exists": true,
    "email": {
        "mx": [{ "priority": 10, "host": "smtp.google.com" }],
        "spf": { "present": true, "record": "v=spf1 include:_spf.google.com ~all", "all": "~all", "dns_lookups": 1 },
        "dmarc": { "record": "v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com", "policy": "reject", "rua_present": true },
        "dkim": { "note": "Best effort: ...", "selectors_found": [] },
        "mta_sts_present": true,
        "bimi_present": false
    },
    "dns": { "caa": ["0 issue \"pki.goog\""], "dnssec_ds_present": false },
    "https": {
        "final_url": "https://www.google.com/",
        "certificate_valid": true,
        "certificate_issuer": "Google Trust Services",
        "certificate_expires": "2026-12-30",
        "days_to_expiry": 62,
        "tls_version": "TLSv1.3",
        "http_redirects_to_https": false,
        "security_headers_missing": ["Strict-Transport-Security", "Content-Security-Policy", "..."]
    },
    "errors": [],
    "score": 73,
    "breakdown": [{ "check": "mx_present", "passed": true, "points": 5, "max": 5 }, "..."],
    "recommendations": [
        "Enable DNSSEC at your DNS host and registrar to protect DNS answers from tampering.",
        "Redirect http:// to https:// for the homepage.",
        "..."
    ],
    "spf_policy": "~all",
    "dmarc_policy": "reject",
    "dkim_selectors_found": [],
    "cert_days_to_expiry": 62,
    "security_headers_missing": ["Strict-Transport-Security", "..."]
}
```

The flat fields at the end (`spf_policy`, `dmarc_policy`, ...) repeat the most used values so CSV and Excel exports are easy to read.

Problems are reported in `errors` for that domain instead of failing the run. One bad domain never stops the others.

### Pricing

Pay per event: you pay for each domain checked (`domain-checked` event), including domains that turn out not to exist, because the lookups were still done. Invalid input lines are not charged. The price is shown on the Actor page. You can cap the cost of a run with the "Maximum cost per run" setting; the Actor stops cleanly when the cap is reached.

### Limitations

- DKIM is best effort. Many providers use custom selectors (for example Google Workspace often uses `google`, but Google itself uses dated selectors), so a missing DKIM result is common and not proof of a problem.
- DNSSEC is a signal (DS record present), not full validation.
- SPF lookup counting follows `include` and `redirect` up to 20 nested records. Macros such as `%{i}` are not expanded.
- MTA-STS and BIMI only check the TXT record, not the policy file or the logo/certificate.
- Only the apex domain (without `www.`) is checked. Subdomains in the input are checked as given.
- The redirect check looks at the first response from `http://<domain>/`. A redirect to `http://www.<domain>/` that later goes to HTTPS counts as "no redirect".
- Some sites block automated requests or answer differently by region. In that case the HTTPS part may show an error while DNS results are still complete.
- Results are a snapshot of public DNS and the homepage at the time of the run.

### Run locally

```bash
python -m venv .venv
.venv/Scripts/pip install -r requirements.txt   # on Linux/macOS: .venv/bin/pip
## put input in storage/key_value_stores/default/INPUT.json
.venv/Scripts/python -m my_actor
python -m unittest discover -s tests -t .
```

Author: Matias Isnard.

# Actor input Schema

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

Domains or URLs to check, one per line. URLs are reduced to their hostname and a leading www. is removed (https://www.example.com/page becomes example.com). Duplicates are checked once.

## `timeoutSecs` (type: `integer`):

Maximum time for each DNS query and each HTTP request.

## `includeHttpChecks` (type: `boolean`):

Fetch the homepage once over HTTPS (certificate, TLS version, security headers) and once over HTTP (redirect check). Turn off for DNS-only results; the score is then scaled from the 70 DNS and email points.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "github.com"
  ],
  "timeoutSecs": 10,
  "includeHttpChecks": true
}
```

# 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": [
        "example.com",
        "github.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("isnard/domain-security-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": [
        "example.com",
        "github.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("isnard/domain-security-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": [
    "example.com",
    "github.com"
  ]
}' |
apify call isnard/domain-security-checker --silent --output-dataset

```

## MCP server setup

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