# Website Health Report: SSL, Headers, DNS (`kelp_forest_labs/website-health-report`) Actor

One row per domain with an A-F grade: SSL certificate expiry and trust, TLS version, HTTP-to-HTTPS redirect, six security headers, and DNS records (IPv6, SPF, DMARC, CAA), each with a plain-English detail. At most a few requests per domain. Unresolvable domains are free.

- **URL**: https://apify.com/kelp_forest_labs/website-health-report.md
- **Developed by:** [kelp forest](https://apify.com/kelp_forest_labs) (community)
- **Categories:** Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 domain reports

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

Give it a list of domains. You get **one graded row per domain** with the checks agencies and IT teams run every
month: certificate expiry and trust, HTTP→HTTPS redirect, security headers and email/DNS hygiene.

### Use cases

- Monthly health reports for agency client portfolios.
- Catch SSL certificates before they expire.
- Track security-header and email-auth (SPF/DMARC) gaps across many domains.
- Due diligence on a list of vendor domains.

### What it checks

| Group | Checks | Result levels |
|---|---|---|
| TLS certificate | days to expiry (warn < 30 days, fail if expired), trusted chain + host-name match, protocol (TLS 1.2/1.3) | pass / warn / fail |
| HTTPS | HTTPS answers; HTTP redirects to HTTPS (301/308 = pass, 302/307 = warn, none = fail) | pass / warn / fail |
| Security headers | Strict-Transport-Security (max-age ≥ 180 days), Content-Security-Policy, X-Content-Type-Options, clickjacking protection (X-Frame-Options or CSP frame-ancestors), Referrer-Policy, Permissions-Policy | pass / warn / fail / info |
| DNS | A/AAAA, IPv6, SPF (one record, no `+all`), DMARC policy, CAA | pass / warn / fail / info |

Each row has `grade` (A–F), `scorePct`, `passed`/`warnings`/`failed`, `failedChecks`, `warningChecks`, the full
`checks` list with a plain-English `detail`, the certificate summary (issuer, expiry, SANs) and the raw security headers.
`info` results (optional items such as CAA) never lower the grade.

### Pricing (pay per event)

| Event | Price |
|---|---|
| `domain` (one domain with a report) | US$0.003 |

**Free:** domains that do not resolve, private/reserved addresses, and sites that answer neither HTTPS nor HTTP get an
error row and no charge.

Each run also has Apify's run-start charge of US$0.00005 per GB of run memory (US$0.00005 at the default 512 MB),
charged when the run starts, even if every domain gets an error row.

### How to use

1. Open the actor in Apify Console and fill in the input form (or paste the JSON example below into the JSON editor).
2. Optional: set a maximum charge for the run under the run options to cap what a run can cost.
3. Click **Start**. Results appear in the **Output** tab as they are produced; export them as JSON, CSV, Excel or HTML.

You can also start runs from the Apify API or CLI, run the actor on a schedule (for example weekly), and send
results to other apps with Apify integrations and webhooks.

### Input example

```json
{"domains": ["example.com", "example.org"], "checkDns": true}
```

### Output example (local fixture run with a self-signed certificate; trimmed to the summary and the failed or warning checks)

```json
{"domain": "https://127.0.0.1:8443",
 "status": "ok",
 "grade": "F",
 "gradeCappedBy": ["https_response", "tls_trust"],
 "scorePct": 50,
 "passed": 2,
 "warnings": 1,
 "failed": 2,
 "failedChecks": ["tls_trust", "https_response"],
 "warningChecks": ["https_redirect"],
 "certificate": {"subjectCN": "localhost", "issuerO": "Kelp Test CA", "daysToExpiry": 199, "protocol": "TLSv1.3", "trusted": false},
 "checks": [{"check": "tls_trust", "result": "fail", "detail": "certificate not trusted for this host: self-signed certificate"}, {"check": "https_response", "result": "fail", "detail": "HTTPS request failed: ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1029)"}, {"check": "https_redirect", "result": "warn", "detail": "plain HTTP not reachable (ConnectError: All connection attempts failed); nothing to redirect"}]}
```

### Light touch

Per domain: two TLS handshakes on port 443 (one reads the certificate, one verifies trust and the host name), one
GET of `https://domain/` and one GET of `http://domain/`, each following at most 8 redirects (every hop is checked
before it is requested), plus DNS lookups. Usually that is 4 connections, at most 20 if both URLs redirect 8 times.
All requests use an identified user agent (`KelpForestLabs-HealthCheck/1.0 ...`). No crawling, no port scanning, no
vulnerability probing.

### How the grade works

Every graded check counts the same: pass = 1, warn = 0.5, fail = 0; `info` results (optional items such as CAA,
IPv6 or Permissions-Policy when absent) are not counted. `scorePct` is the average and sets the grade: A ≥ 90,
B ≥ 80, C ≥ 65, D ≥ 50, otherwise F. **A failed critical check caps the grade at F** whatever the average: an expired
or untrusted certificate, a failed TLS handshake, or no HTTPS response, because visitors see a browser error.
`gradeCappedBy` names the check(s), and `failedChecks` always lists every failure.

Examples, computed with the actor's own scoring code:

| Situation | Graded checks | scorePct | Grade |
|---|---|---|---|
| everything passes, IPv6 and CAA present | 16 pass | 100 | A |
| no CSP and no Permissions-Policy (otherwise good) | 12 pass, 1 warn | 96 | A |
| no HTTP→HTTPS redirect, only HSTS set, no DMARC | 7 pass, 4 warn, 2 fail | 69 | C |
| expired certificate, otherwise good | 13 pass, 1 fail | 93 | F (`gradeCappedBy: ["tls_expiry"]`) |

### FAQ

**Is this a security scan?**
No. It reads public TLS, DNS and header information only. A good grade does not mean a site is secure.

**Which name should I enter?**
Use the bare domain (example.com) so SPF and DMARC are looked up on the right name.

**What does an unreachable domain cost?**
Nothing. Domains that do not resolve, private addresses and sites answering neither HTTPS nor HTTP are free error rows.

**How is the grade calculated?**
Every graded check counts the same: pass = 1, warn = 0.5, fail = 0, `info` is not counted. `scorePct` is the average; A >= 90, B >= 80, C >= 65, D >= 50, otherwise F. A failed critical check (an expired or untrusted certificate, a failed TLS handshake, or no HTTPS response) caps the grade at F whatever the average, and `gradeCappedBy` names it. `failedChecks` always lists every failure.

### Limits: what this actor does not do

- It is not a penetration test or a compliance certificate; a good grade does not mean a site is secure.
- It checks only the home page response for headers.
- SPF/DMARC/MX are looked up for the name you give: use the bare domain (`example.com`), not `www.example.com`.
- A port in the input (`https://example.com:8443`) is used for the TLS and HTTPS checks; the HTTP-to-HTTPS
  redirect check always uses `http://` on port 80.
- The TLS probe and each HTTP request must finish within 30 seconds, so one slow server cannot hold up the list.

### Changelog

- 1.0 (October 2026): first public version.

# Actor input Schema

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

e.g. example.com or https://example.com. Use the bare domain (not www.) to get SPF/DMARC/MX results for the domain.

## `checkDns` (type: `boolean`):

Look up A, AAAA, MX, CAA, SPF and DMARC records.

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

Applies to each TLS handshake and HTTP request.

## Actor input object example

```json
{
  "domains": [
    "apify.com"
  ],
  "checkDns": true,
  "timeoutSecs": 10
}
```

# Actor output Schema

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

One row per domain with the A-F grade and every check (SSL, TLS, redirect, security headers, DNS).

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kelp_forest_labs/website-health-report").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"] }

# Run the Actor and wait for it to finish
run = client.actor("kelp_forest_labs/website-health-report").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"
  ]
}' |
apify call kelp_forest_labs/website-health-report --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kelp_forest_labs/website-health-report"
        }
    }
}
```

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/MSTwRqf7Agbhaq1y1/builds/VKiHWMM7ehw1JjLxk/openapi.json
