# DNS Lookup & SSL Certificate Checker (`mouadapi/dns-ssl-checker`) Actor

Bulk DNS lookup and SSL certificate checker: one flat row per domain with DNS records (A, AAAA, NS, MX, SPF, DMARC, CAA), SSL expiry, issuer, chain, hostname match and problems found, never charged for failed results. Tracks changes between runs.

- **URL**: https://apify.com/mouadapi/dns-ssl-checker.md
- **Developed by:** [COMPASS DEV](https://apify.com/mouadapi) (community)
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

Bulk **DNS lookup** and **SSL certificate checker** that returns one flat row per domain with its **DNS records** (A,
AAAA, NS, MX, SPF, DMARC, CAA) and SSL certificate expiry, issuer, chain and hostname match — a quick **domain health**
check for many domains in one run — and never charges for failed results.

Every domain gets one flat row with a short list of **problems found** ("certificate expires in 12 day(s)", "no DMARC
record", "hostname mismatch"...). Give it a list name and it **tracks changes between runs**: renewed or changed
certificates, new mail servers, DNS moves.

**You are never charged for failed results**: domains that don't exist, invalid inputs and DNS failures are free.

### Quick start

1. Click **Start**. The form is already filled in with two domains:

   ```json
   { "domains": ["apify.com", "wikipedia.org"], "expiryWarningDays": 30, "maxItems": 1000 }
   ```

2. In under a minute you get two rows, one per domain (see [Output](#output)). On the Free plan this run costs
   $0.003 plus Apify's Actor-start charge.

3. Replace the two domains with your own (domains or URLs, one per line) and run it again. Export the results as CSV,
   Excel or JSON from the **Output** tab.

### Use cases

- **SSL certificate expiry watch.** Check all your domains every week and see which certificates expire within the
  next 30 days (`expiresSoon`, `daysToExpiry`), before a visitor sees a browser warning.
- **SPF and DMARC audit.** Check a list of domains that send email (your brands, or your clients' domains) for a
  missing SPF record, several SPF records, no DMARC record or a DMARC policy of `none`.
- **DNS change monitoring.** After a migration or a DNS provider change, check that every domain resolves and that
  its name servers and mail servers are what you expect; with a `stateName` and `"onlyChanged": true`, later runs
  show only what changed.

### What it does

- **One row per domain** with a clear status: `ok` (the domain resolves; charged, even when HTTPS is missing — the row
  says so), `no_data` (the domain doesn't exist or has no records; free) or `failed` (invalid input or DNS failure;
  free).
- **DNS:** A and AAAA addresses, name servers, mail servers with priority, the SPF record, the DMARC record and policy,
  and the CAs allowed to issue certificates (CAA).
- **SSL/TLS:** one TLS handshake to port 443 (the same first step a browser takes; no page is downloaded): certificate
  issuer, subject, the names it covers, valid from/to, days to expiry, hostname match, chain trusted (and why not),
  TLS version.
- **Problems** in plain words, and `expiresSoon` when the certificate expires within `expiryWarningDays` (default 30).
- **Change tracking:** with `stateName`, every row says whether the domain is new or changed since the last run with
  that name and which fields changed; with `onlyChanged`, unchanged domains are left out (free). Schedule it daily to
  get only what changed.
- URLs work too: only the host name is checked (`https://www.example.org/pricing` → `www.example.org`).

### Input

Check two domains:

```json
{ "domains": ["apify.com", "wikipedia.org"] }
```

Daily watch of client domains, warn 21 days before a certificate expires, output only what changed:

```json
{
    "domains": ["example.org", "shop.example.org", "https://www.example.net/"],
    "expiryWarningDays": 21,
    "stateName": "client-domains",
    "onlyChanged": true
}
```

| Field | Default | Description |
|---|---|---|
| `domains` | — | Required. Domain names or URLs, one per line. |
| `expiryWarningDays` | `30` | Days before expiry that set `expiresSoon` and add a problem (0–365). |
| `maxItems` | `1000` | Most domains checked in one run (1–5,000); the rest are listed in the log and not checked. |
| `stateName` | — | Name of a tracked list; enables the change fields. |
| `onlyChanged` | `false` | With `stateName`: output only new or changed domains. |
| `maxConcurrency` | `5` | Domains checked at the same time (1–10). |

**Field names from other tools:** `urls`, `targets`, `startUrls`, `hostnames` (→ `domains`); `warningThresholdDays`
(→ `expiryWarningDays`); `maxResults` (→ `maxItems`).

### Output

```json
{
    "status": "ok",
    "error": null,
    "attempts": 1,
    "input": "wikipedia.org",
    "domain": "wikipedia.org",
    "aRecords": "185.15.58.224",
    "aaaaRecords": "2a02:ec80:600:ed1a::1",
    "nsRecords": "ns0.wikimedia.org, ns1.wikimedia.org, ns2.wikimedia.org",
    "mxRecords": "10 mx-in1001.wikimedia.org, 10 mx-in2001.wikimedia.org",
    "spfRecord": "v=spf1 include:_cidrs.wikimedia.org ~all",
    "dmarcRecord": "v=DMARC1; p=reject; rua=mailto:…@wikimedia.org;",
    "dmarcPolicy": "reject",
    "caaRecords": "issue letsencrypt.org, issue pki.goog",
    "tlsStatus": "ok",
    "tlsAddress": "185.15.58.224",
    "tlsVersion": "TLSv1.3",
    "certIssuer": "Let's Encrypt (YE2)",
    "certSubject": "*.wikipedia.org",
    "certSans": "*.wikipedia.org, wikipedia.org, *.wikimedia.org, wikimedia.org, …",
    "certValidFrom": "2026-08-05T19:15:41.000Z",
    "certValidTo": "2026-11-03T19:15:40.000Z",
    "daysToExpiry": 35,
    "hostnameMatch": true,
    "chainTrusted": true,
    "chainError": null,
    "expiresSoon": false,
    "problems": null,
    "problemCount": 0,
    "firstSeen": null,
    "changedSinceLastRun": null,
    "changedFields": null,
    "url": "https://wikipedia.org",
    "scrapedAt": "2026-09-29T14:15:23.517Z"
}
```

| Status | Meaning | Charged? |
|---|---|---|
| `ok` | The domain resolves (any record). TLS details, or why there are none (`tlsStatus`: `no_https`, `timeout`, `handshake_failed`, `no_address`) | Yes |
| `no_data` | The domain doesn't exist (NXDOMAIN) or has no records | No |
| `failed` | Invalid input (IP address, localhost, internal or reserved name), only private addresses, DNS SERVFAIL or timeout (`error` says why) | No |

Problems it reports: no A or AAAA record; no SPF record, several SPF records, no DMARC record or DMARC policy `none`
(for domains that receive mail); no HTTPS, HTTPS timeout, TLS handshake failure; certificate expired or expiring,
hostname mismatch, untrusted chain, old TLS version (1.0/1.1); a record type whose lookup failed.

Every row has `url` (the **source URL**: `https://<domain>`, or null when the input is not a valid host name) and
`scrapedAt` (the **fetch time**). The key-value store holds `RUN_REPORT` (counts, charged and free rows, DNS and TLS
timings).

### Pricing

Pay per domain checked (event `domain-check`), only when the domain resolves. **Never charged for failed results**:
`no_data` and `failed` rows, domains beyond `maxItems` and unchanged domains left out by `onlyChanged` are free.

| Apify plan | Price per 1,000 domains |
|---|---|
| Free / no discount | $1.50 |
| Bronze | $1.30 |
| Silver | $1.15 |
| Gold (and Platinum, Diamond) | $1.00 |

No platform usage fees on top: the price per domain covers compute. A run also has Apify's small Actor-start charge.

### Limits

- Up to 5,000 domains per run (`maxItems`, default 1,000).
- One TLS handshake per domain, to one public address (IPv4 first) on port 443 with the domain as SNI. Other ports,
  other addresses of the same domain and `www.` variants are not checked; add them as their own domains.
- No web page is downloaded and no HTTP request is sent, so HTTP redirects, HSTS and page content are not checked.
- No WHOIS/RDAP (registrar, registration dates).
- Chain trust uses the Mozilla CA store shipped with Node.js.
- IP addresses, localhost, internal names (.local, .internal, .lan...), reserved test names (.test, .example, .invalid)
  and names that resolve only to private addresses are not checked (free failed row).

### Known issues

- DMARC and SPF are checked on the name you give; DMARC inherited from a parent domain is not looked up.
- A domain whose name servers answer slowly can give a DNS timeout (free failed row); run it again later.

### Use it from AI agents

- **MCP:** add the Actor to your agent through the Apify MCP server (e.g. `https://mcp.apify.com?actors=mouadapi/dns-ssl-checker`),
  then ask: *"Check the SSL certificates and DMARC of example.org and shop.example.org; list anything that expires within 30
  days."* Each row says `ok`, `no_data` or `failed`, and `problems` is plain text an agent can quote.
- **API:** one call runs the check and returns the rows:

```bash
curl -X POST "https://api.apify.com/v2/acts/mouadapi~dns-ssl-checker/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" -d '{"domains": ["example.org"], "expiryWarningDays": 30}'
```

- **x402 payments:** the Actor is pay-per-event only, with no usage fees, limited permissions and no Standby mode, so
  agents can pay per domain with x402.

The same call from JavaScript (the Apify client):

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('mouadapi/dns-ssl-checker').call({ domains: ['apify.com'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

### FAQ

**How much does it cost?** $1.50 per 1,000 domains on the Free plan, down to $1.00 on Gold (see [Pricing](#pricing)),
plus Apify's small Actor-start charge per run. Checking 10 domains that resolve costs $0.015 on the Free plan.

**Am I charged when a check fails?** No. Only `ok` rows (the domain resolves) are charged. `no_data` and `failed` rows,
domains beyond `maxItems`, and unchanged domains left out by `onlyChanged` are free.

**How many domains can I check?** Up to 5,000 per run (default 1,000). In our tests, 1,000 domains took about 6 minutes
with the default settings.

**Does it download my website?** No. It sends DNS queries and makes one TLS handshake per domain; no web page is
requested. See [Limits](#limits) for what that leaves out.

**How do I watch my domains every week?** Save your list as an Apify task with a `stateName`, and add an Apify schedule
(for example, every Monday). Each run's rows say what changed; filter on `expiresSoon` or `problemCount` to see what
needs action.

### Data source

Public DNS (through the platform's resolver) and the TLS handshake of each domain's own server. No third-party API.
Report addresses in DMARC records keep only their domain (`mailto:…@example.org`), and CAA contact tags are not output.

Also by the same author: [Woolworths Price Scraper & Monitor](https://apify.com/mouadapi/woolworths-price-monitor) —
Woolworths (Australia) product prices, specials and price changes.

### Changelog

- 0.1: first version: DNS records, TLS certificate, problems, change tracking.

# Actor input Schema

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

Required. Domain names or URLs, one per line (for a URL only the host name is kept: https://www.example.org/page → www.example.org). Each gives one row: ok (the domain resolves; charged), no\_data (the domain doesn't exist or has no records; free) or failed (invalid input, IP address, private address, DNS failure; free).

## `expiryWarningDays` (type: `integer`):

A certificate that expires within this many days sets expiresSoon and is listed in problems. Default 30.

## `maxItems` (type: `integer`):

Most domains checked in one run; the rest are listed in the log and not checked (free). Default 1,000.

## `stateName` (type: `string`):

Name a watchlist (e.g. "client-domains") to compare with the previous run of the same name: every row gets firstSeen, changedSinceLastRun and changedFields (records, certificate issuer and expiry, TLS version, problems). Stored in your own Apify storage.

## `onlyChanged` (type: `boolean`):

With a list name: leave out domains that did not change since the last run (free).

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

Domains checked at the same time (DNS queries plus one TLS handshake each).

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "wikipedia.org"
  ],
  "expiryWarningDays": 30,
  "maxItems": 1000,
  "onlyChanged": false,
  "maxConcurrency": 5
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset with one row per input item

## `runReport` (type: `string`):

Summary of the run (counts, charged and free rows, stop reason)

# 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",
        "wikipedia.org"
    ],
    "expiryWarningDays": 30,
    "maxItems": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("mouadapi/dns-ssl-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",
        "wikipedia.org",
    ],
    "expiryWarningDays": 30,
    "maxItems": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("mouadapi/dns-ssl-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",
    "wikipedia.org"
  ],
  "expiryWarningDays": 30,
  "maxItems": 1000
}' |
apify call mouadapi/dns-ssl-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mouadapi/dns-ssl-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/AgfA6Ihqf1kymfR32/builds/XJ6FyBAq2a2yfmZwp/openapi.json
