# Bulk Website Audit: 1,000 Sites for $3 (SEO, SSL, DMARC) (`quanta1810/website-health-audit`) Actor

Audit 1,000 websites for $3: one row per site with a 0-100 health score, top issues and public contact emails — SEO, SSL expiry, security headers, SPF/DMARC, sitemap. For web design leads and client-site monitoring. $0.003/site, failed sites free. Works via Apify MCP and x402.

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

## Pricing

from $3.00 / 1,000 website auditeds

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

## Bulk Website Audit: 1,000 Sites for $3 (SEO, SSL, DMARC)

Paste a list of domains, get **one clean row per website** with a **0–100 health score** and the **top 3 problems written in plain English** — ready to drop into a spreadsheet, a client report or a cold-outreach email.

Every site is checked for **10 things in a single pass**:

| Area | What is checked |
|---|---|
| **Availability** | HTTP status, full redirect chain, HTTP → HTTPS redirect, response time, page size |
| **SSL certificate** | Valid / expired / self-signed, issuer, expiry date, **days left** |
| **On-page SEO** | Title & meta description (with lengths), H1, canonical, `noindex`, `lang`, mobile viewport, favicon |
| **Social previews** | Open Graph title & image, Twitter card |
| **Structured data** | Schema.org JSON-LD types found (Organization, Product, LocalBusiness, …) |
| **Content** | Word count, images missing `alt`, internal / external link counts |
| **Crawlability** | `robots.txt` present, "Disallow: /" for all bots, XML sitemap found + number of URLs |
| **Security headers** | HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy → grade A–F |
| **Email DNS** | MX, **SPF**, **DMARC** (and its policy), NS, CAA, IPv4/IPv6 |
| **Public contact emails** | Addresses the site itself lists (mailto links / visible text on the homepage, or its contact page) — role addresses like `info@` first |

No browser, no proxy, no API keys. It only reads public pages and public DNS, so it is **fast (about 50 sites per minute at the default 512 MB, roughly twice that with 1 GB and *Parallel sites* set to 20)** and **cheap: $0.003 per site, and sites that fail are free**.

### Who is it for?

- **Agencies prospecting for clients** (web design, SEO, IT support): paste a list of local businesses, sort by health score, get the contact email each site publishes, and open your first email with the exact problem — "your SSL certificate expires in 9 days", "Google can't index your homepage", "you have no DMARC record".
- **Portfolio monitoring** (agencies managing client sites, franchises, holding companies, SaaS with many landing domains): schedule a weekly run in **monitoring mode** — it reports only the sites that changed (a renewed or expiring certificate, a broken homepage, a new `noindex`, a lost DMARC record) and unchanged sites cost one sixth of the price.
- **Email deliverability & IT teams**: spot domains without SPF / DMARC or with `p=none`.
- **Due diligence**: quick technical snapshot of a list of companies' websites.

### Quick links

- [Find web design & SEO leads](#find-web-design--seo-leads-step-by-step) · [Weekly monitoring (only what changed)](#weekly-monitoring-report-only-what-changed) · [Use with n8n, Make, Zapier, Google Sheets](#use-it-with-n8n-make-zapier-or-google-sheets) · [Contact emails](#contact-emails) · [All output columns](#output-columns) · [For AI agents](#for-ai-agents-mcp-x402) · [Pricing & cost control](#pricing) · [FAQ](#faq)

### How to use

1. Add your websites in **Websites** — one per line. Bare domains (`example.com`) or full URLs (`https://www.example.com`) both work; duplicates are removed automatically (`www.example.com` and `example.com` count as one site).
2. (Optional) Turn off DNS, sitemap or contact-email collection if you don't need them.
3. Click **Start**. Download the results as **CSV, Excel, JSON** or use the API.

#### Input example

```json
{
  "websites": ["apify.com", "example.com", "https://expired.badssl.com", "tiki.vn"],
  "checkDns": true,
  "checkSitemap": true,
  "extractContacts": true
}
```

#### Output example (one row, shortened)

```json
{
  "domain": "tiki.vn",
  "status": "ok",
  "charged": true,
  "reachable": true,
  "healthScore": 88,
  "issueCount": { "high": 0, "medium": 1, "low": 3 },
  "topIssues": "Security headers grade F (0/6 present) | Incomplete Open Graph tags (poor link previews) | No HSTS header",
  "contactEmails": ["hotro@tiki.vn", "security@tiki.vn"],
  "contactPage": "https://tiki.vn/",
  "finalUrl": "https://tiki.vn/",
  "statusCode": 200,
  "redirectCount": 1,
  "responseTimeMs": 1308,
  "blockedByBotProtection": false,
  "behindLogin": false,
  "loginProvider": null,
  "jsRendered": false,
  "ssl": { "valid": true, "issuer": "DigiCert Inc", "validTo": "2027-04-17T23:59:59.000Z", "daysLeft": 187 },
  "securityHeaders": { "grade": "F", "present": 0, "hsts": false, "csp": false },
  "seo": { "title": "…", "titleLength": 54, "h1Count": 1, "hasViewport": true, "structuredDataTypes": ["Store", "Organization", "WebSite", "LocalBusiness", "Service", "BreadcrumbList"] },
  "crawl": { "robotsTxt": true, "robotsBlocksAll": false, "sitemapFound": true, "sitemapUrlCount": 6 },
  "dns": { "mx": ["aspmx.l.google.com", "…"], "spf": "v=spf1 include:_spf.google.com …", "dmarcPolicy": "none" },
  "issues": [ { "severity": "medium", "code": "WEAK_SECURITY_HEADERS", "message": "Security headers grade F (0/6 present)" }, "…" ]
}
```

#### What a results table looks like

| domain | healthScore | topIssues | behindLogin | jsRendered |
|---|---|---|---|---|
| tiki.vn | 88 | Security headers grade F (0/6 present) · Incomplete Open Graph tags · No HSTS header | false | false |
| tuoitre.vn | 82 | No H1 heading · No viewport meta tag (not mobile-friendly) · Title is 74 chars | false | false |
| shopee.vn | 74 | Content is rendered by JavaScript — SEO checks below may be incomplete · No XML sitemap found | false | **true** |
| intranet.example.com | *null* | Behind a login wall — only public pages can be audited (Cloudflare Access) | **true** | false |
| no-such-site.example | *null* | *(unreachable: ENOTFOUND — not charged)* | – | – |

#### Status flags

| Field | Meaning |
|---|---|
| `status` | One word per row so you can filter fast: `ok` (audited), `http_error` (audited — the homepage answered 4xx/5xx), `unchanged` (monitoring mode: re-checked, nothing changed — short row), `blocked` (bot protection, firewall or login wall), `unreachable`, `invalid` (input line is not a domain), `skipped` (your *Max cost per run* was reached), `error`. **Only `ok`, `http_error` and `unchanged` are charged.** |
| `charged` | `true` when this row was billed. Every other row is free. |
| `reachable` | `false` when the site could not be reached at all (DNS error, timeout). The reason is in `error`. **Not charged.** |
| `blockedByBotProtection` | The homepage answered with an anti-bot challenge (Cloudflare, Akamai, AWS WAF, DataDome…) or refused automated requests (HTTP 401/403/405/429 — the site may still work in a browser). SSL, DNS and headers are still reported; on-page SEO is skipped and `healthScore` is `null`. **Not charged.** |
| `behindLogin` | The homepage redirects to a login / SSO page (Cloudflare Access, Google, Microsoft, Okta, Auth0, HTTP authentication, or a 401/403 login form). `loginProvider` says which. The site's own SSL certificate and DNS are still checked, but SEO and robots.txt checks are skipped (they would describe the login page, not the site) and `healthScore` is `null`. **Not charged.** |
| `jsRendered` | The homepage HTML is a JavaScript app shell (React / Next.js / Vue / Angular) with almost no text until scripts run. The Actor does not run a browser, so a missing title, meta description or H1 in the raw HTML is reported as **low** severity instead of high, with a note that SEO checks may be incomplete. SSL, headers, DNS, redirects, robots.txt and sitemap are fully audited, so this row **is charged**. |

The dataset has two ready-made table views: **Overview** (score, top issues, contact emails, SSL days left, security grade, DMARC, sitemap) and **On-page SEO**.

### Output columns

Every row has the same columns (nested objects flatten to `ssl.daysLeft`, `dns.dmarcPolicy`… in CSV / Excel). Fields that could not be measured are `null`.

**Summary**

| Column | Type | Meaning |
|---|---|---|
| `input` | text | The website exactly as you entered it |
| `domain` | text | Hostname that was audited |
| `status` | text | `ok` · `http_error` · `unchanged` · `blocked` · `unreachable` · `invalid` · `skipped` · `error` (see *Status flags*) |
| `charged` | true/false | Whether this row was billed |
| `reachable` | true/false | `false` = DNS error or timeout (reason in `error`) |
| `healthScore` | 0–100 | 100 minus 15 per high, 6 per medium, 2 per low issue; `null` when the homepage could not be analysed |
| `issueCount` | object | `high`, `medium`, `low` counts |
| `topIssues` | text | Up to 3 most important problems in plain English, separated by `\|` |
| `contactEmails` | list | Contact emails the website publishes itself — lower-case, deduplicated, up to 10, role addresses (`info@`, `contact@`, `sales@`…) and the site's own domain first. `[]` = none listed; `null` = not collected (option off, or the homepage could not be analysed) |
| `contactPage` | text | Page where `contactEmails` were found (the homepage or its contact / about page); `null` when none were found |
| `issues` | list | Every issue: `severity`, `code`, `message` (codes listed below) |
| `error` | text | Why a site was not audited (e.g. `ENOTFOUND`, `Timeout`) |
| `change`, `changes` | text, list | Monitoring mode only: `new` / `changed` / `unchanged` and what changed (see [Weekly monitoring](#weekly-monitoring-report-only-what-changed)); `null` for sites that could not be audited |
| `chargedEvent` | text | Monitoring mode only: `site-audited` or `site-unchanged` |
| `monitorName`, `firstSeenAt`, `lastChangedAt` | text, date | Monitoring mode only: the list name, when the site was first seen in it, when it last changed |
| `checkedAt` | date | When the site was checked (UTC) |

**Availability**

| Column | Meaning |
|---|---|
| `finalUrl` | Homepage URL after all redirects |
| `statusCode` | HTTP status of the homepage |
| `redirectCount`, `redirectChain` | Number of redirects and each hop (`url`, `status`) |
| `responseTimeMs` | Time to fetch the homepage HTML incl. redirects (not a Lighthouse metric) |
| `htmlSizeKb` | Size of the homepage HTML |
| `server`, `poweredBy` | `Server` and `X-Powered-By` headers |
| `noindex` | `X-Robots-Tag: noindex` header present |
| `blockedByBotProtection`, `behindLogin`, `loginProvider`, `jsRendered` | See *Status flags* above |
| `dnsResolves` | Only on unreachable rows: whether the domain has DNS records at all |

**SSL certificate — `ssl.*`**: `valid`, `error`, `issuer`, `validTo`, `daysLeft` (negative = expired), `protocol`.

**Security headers — `securityHeaders.*`**: `grade` (A = all 6 … F = none), `present` (0–6), and true/false for `hsts`, `csp`, `xFrameOptions`, `xContentTypeOptions`, `referrerPolicy`, `permissionsPolicy`.

**On-page SEO — `seo.*`** (homepage HTML, no JavaScript executed): `title`, `titleLength`, `metaDescription`, `metaDescriptionLength`, `h1Count`, `h1`, `canonical`, `noindex`, `lang`, `hasViewport`, `hasFavicon`, `openGraph.title`, `openGraph.image`, `twitterCard`, `hreflangCount`, `structuredDataTypes`, `imageCount`, `imagesMissingAlt`, `internalLinks`, `externalLinks`, `wordCount`, `generator`.

**Crawlability — `crawl.*`**: `robotsTxt`, `robotsBlocksAll`, `sitemapFound`, `sitemapUrl`, `sitemapUrlCount`, `sitemapIsIndex`.

**Email & DNS — `dns.*`**: `apexDomain`, `ipv4`, `ipv6`, `nameservers`, `mx`, `spf`, `dmarc`, `dmarcPolicy` (`none` / `quarantine` / `reject` / `null`), `caa`.

**Issue codes** (in `issues[].code`, stable — safe to filter on): `HTTP_ERROR`, `BOT_PROTECTION`, `BEHIND_LOGIN`, `NO_HTTPS`, `NO_HTTPS_REDIRECT`, `LONG_REDIRECT_CHAIN`, `SLOW_RESPONSE`, `SSL_INVALID`, `SSL_EXPIRING` (< 5 days), `SSL_EXPIRING_SOON` (< 14 days), `JS_RENDERED`, `NOINDEX`, `MISSING_TITLE`, `TITLE_TOO_LONG`, `TITLE_TOO_SHORT`, `MISSING_META_DESCRIPTION`, `META_DESCRIPTION_TOO_LONG`, `MISSING_H1`, `MULTIPLE_H1`, `MISSING_CANONICAL`, `NOT_MOBILE_FRIENDLY`, `MISSING_LANG`, `MISSING_OPEN_GRAPH`, `NO_STRUCTURED_DATA`, `IMAGES_MISSING_ALT`, `THIN_CONTENT`, `ROBOTS_BLOCKS_ALL`, `NO_ROBOTS_TXT`, `NO_SITEMAP`, `WEAK_SECURITY_HEADERS`, `NO_HSTS`, `NO_SPF`, `NO_DMARC`, `DMARC_POLICY_NONE`.

### Find web design & SEO leads (step by step)

1. Get a list of local businesses with websites — for example run the **Google Maps Scraper** on Apify for "dentist in Austin" or "restaurants in Hanoi" and export the `website` column.
2. Paste those websites into this Actor and run it. A few hundred sites take a few minutes and cost about a dollar.
3. Sort the results by `healthScore` (lowest first) or filter by issue `code` — e.g. `SSL_EXPIRING_SOON`, `NO_DMARC`, `MISSING_META_DESCRIPTION`, `NOT_MOBILE_FRIENDLY`.
4. Use `topIssues` as the first line of your outreach email and send it to the address in `contactEmails`: a concrete, verifiable problem beats a generic pitch.

Tip: chain the two Actors with an Apify **integration** (Actor-to-Actor) so every new Maps scrape is audited automatically.

### Contact emails

With **Collect public contact emails** on (default, `extractContacts: true`, no extra cost) every audited site also gets `contactEmails` and `contactPage`:

- Only addresses the website **publishes itself**: `mailto:` links and visible text on the homepage. If the homepage lists none, the Actor makes **one** extra request to the most obvious contact / about page linked from it (same domain, e.g. `/contact`, `/lien-he`, `/kontakt`, `/impressum`, `/about`).
- Simple obfuscations are decoded (`info [at] example [dot] com`, Cloudflare email protection). Nothing is guessed or generated, there are no third-party lookups, no social profiles and no phone numbers.
- Junk is filtered out: image file names (`logo@2x.png`), placeholder addresses (`you@example.com`), tracking IDs and no-reply addresses. Role addresses (`info@`, `contact@`, `sales@`, `support@`) are listed first, up to 10 per site.
- Sites that are unreachable, bot-protected, behind a login or answer with an error get `contactEmails: null`. The contact-page request never changes a site's `status`, score or charge.

**Use it responsibly.** The Actor only collects business contact addresses that websites list publicly for people to contact them. You are responsible for how you use them: follow the GDPR, CAN-SPAM, CASL and your local laws (identify yourself, give an easy opt-out, don't send bulk spam). Turn the option off if you don't need emails.

### Weekly monitoring (report only what changed)

Turn on **Monitoring mode** (`monitorMode: true`) for a list you check every week or month. The Actor remembers each site's last result and the next run tells you **what changed**:

- **New or changed site** → the full row, plus `change: "new"` or `"changed"` and a `changes` list in plain English, e.g. `"SSL expiry date: 2026-10-26 → 2027-01-24"`, `"issues: +NO_DMARC -MISSING_H1"`, `"HTTP status: 200 → 503"`, `"contact emails: +info@example.com"`. Billed **$0.003** (event *Website audited*), like a normal audit.
- **Unchanged site** → a short row with `status: "unchanged"` (score, top issues, SSL days left, contact emails, `lastChangedAt`), so you can see every site was checked. Billed **$0.0005** (event *Website re-checked, unchanged*). Turn off **List unchanged sites** (`includeUnchanged: false`) to get only new and changed sites in the dataset — unchanged sites are still checked, billed and counted in the run's `MONITOR_SUMMARY` record.
- **Blocked or unreachable site** → free as always, and its last remembered result is kept (a site that is down for one run does not "forget" its history).

**Cost example:** 1,000 client sites checked every week, about 10% changing → 100 × $0.003 + 900 × $0.0005 ≈ **$0.75 per week** instead of $3.

> Until the *Website re-checked, unchanged* event is listed in this Actor's **Pricing** tab, unchanged sites are billed like a normal audit ($0.003) — a monitoring run never costs more than a normal run.

**What counts as a change** (anything else — page text, word counts, response time, days left on the certificate — does not):

| Field | Change when… |
|---|---|
| `status`, HTTP status, final host | the homepage starts / stops failing, or redirects to another host |
| issues | an issue `code` appears or disappears (`SLOW_RESPONSE` is ignored — response time varies from week to week). `healthScore` old → new is shown alongside |
| SSL | validity, the expiry **date** (a renewal counts), or the certificate entering the last 14 days |
| security headers | the A–F grade |
| email DNS | SPF present / missing, DMARC policy (`missing` / `none` / `quarantine` / `reject`) |
| robots & sitemap | robots.txt present, "Disallow: /" for all, sitemap found |
| contact emails | an address is added or removed |

A check that was turned off in one of the runs (e.g. DNS) is not compared, so changing options never creates fake changes.

**How to set it up:** save your list as a **task** with Monitoring mode on, then add an Apify **Schedule** (e.g. every Monday 8:00). Use the **Monitoring: changes** dataset view, and connect the **Slack** or **email** integration or a webhook to hear about changes before your client does.

- **Several lists:** give each one its own **Monitor name** (`monitorName`, e.g. `client-acme`); each name is remembered separately.
- **Where the memory lives:** in a key-value store named `website-health-audit-monitor` in **your own** Apify account (one small record per list and shard; nothing is shared with the developer). Delete a list's records there to start it over. Sites that are not in your list for over 400 days are forgotten.
- The first run of a list reports every site as `new` (full price).

### Use it with n8n, Make, Zapier or Google Sheets

Every run produces a normal Apify dataset, so it works with all Apify integrations:

- **n8n** — use the Apify node: *Run an Actor* → *Get dataset items*, then send rows to Google Sheets, Airtable or your CRM.
- **Make / Zapier** — use the Apify app (Make: *Watch Actor Runs* / *Run an Actor*; Zapier: trigger *Finished Actor Run* / action *Run Actor*), then map `domain`, `healthScore` and `topIssues`.
- **Google Sheets / Excel** — download the dataset as CSV/Excel, or use an integration to push rows to a sheet after every run.
- **API (small lists, up to ~100 sites)** — `POST https://api.apify.com/v2/acts/quanta1810~website-health-audit/run-sync-get-dataset-items?token=YOUR_TOKEN` with the input JSON returns the results in one call (synchronous calls time out after 300 s).
- **API (bulk)** — `POST …/acts/quanta1810~website-health-audit/runs?token=YOUR_TOKEN`, then poll the run and read `GET …/datasets/{defaultDatasetId}/items`, or attach a webhook for *run succeeded*.

### For AI agents (MCP, x402)

This Actor works from AI agents through the [Apify MCP server](https://mcp.apify.com) and can be paid per call with x402 (it runs with limited permissions and only reads public web pages and DNS).

- **Minimal input:** `{"websites": ["example.com", "another-shop.com"]}` — optional `maxSites` caps how many are audited.
- **Cost:** $0.003 per audited site; unreachable, bot-protected and login-walled sites are free. 50 sites ≈ $0.15.
- **Speed:** up to ~50 sites usually finish in 1–2 minutes, so the synchronous `run-sync-get-dataset-items` endpoint works for small lists (it times out after 300 s).
- **Read first:** `healthScore` (0–100), `topIssues` (plain English), `contactEmails` (public contact addresses), `issues[].code` (stable codes above), `status` (`ok` / `http_error` audited; anything else was not audited).
- **Example output row (shortened):** `{"domain":"example.com","status":"ok","healthScore":58,"topIssues":"HTTPS works but HTTP does not redirect to it | Missing meta description | No H1 heading","ssl":{"daysLeft":74},"dns":{"dmarcPolicy":"reject"}}`

### How the health score works

Each site starts at 100. Every issue found subtracts points by severity: **high −15** (site down, invalid / expiring SSL, `noindex`, robots.txt blocks everything, no HTTPS), **medium −6** (no sitemap, no meta description, no H1, not mobile-friendly, missing SPF / DMARC, slow response), **low −2** (length and polish issues). The full list of issues with machine-readable `code`s is in the `issues` field, so you can filter by exactly what you sell.

### Pricing

Pay only for results: **$0.003 per website audited** ($3 per 1,000 sites). Sites that are unreachable, behind bot protection or behind a login wall are **not charged**. There is no start fee.

- Sites that **can't be reached** (domain doesn't exist, connection timeout) are listed in the results with the reason, but **are not charged**.
- Sites hidden behind **bot protection** (Cloudflare / Akamai / AWS WAF challenge pages, or a firewall answering 401/403/405/429) are listed with `blockedByBotProtection: true`, SSL / DNS / header data and `healthScore: null` — **not charged** either, because the real homepage could not be analysed.
- A homepage that is **down or broken** (HTTP 5xx with no challenge page, or 404/410) **is** audited and charged (`status: "http_error"`): it gets a high-severity `HTTP_ERROR` — for monitoring and lead generation a broken homepage is the most valuable finding.
- Sites **behind a login wall** (SSO, Cloudflare Access, password prompt) are listed with `behindLogin: true` and `healthScore: null` — **not charged**.
- **Monitoring mode:** an **unchanged** site costs **$0.0005** instead of $0.003 (see [Weekly monitoring](#weekly-monitoring-report-only-what-changed)); new and changed sites cost $0.003.
- On the Apify Free plan, the $5 monthly credit covers about **1,600 audited sites** — trying it is effectively free.

#### Cost control: max sites and max cost

| Sites audited | Cost |
|---|---|
| 100 | $0.30 |
| 1,000 | $3 |
| 10,000 | $30 |

- **Max sites to audit** (`maxSites`, up to 50,000) audits only the first N sites of your list — handy for a quick test of a big list.
- **Max cost per run** (run options, or `maxTotalChargeUsd` in the API) is a hard budget: the Actor reserves budget per site before auditing it, never goes over, and lists the sites that did not fit as `status: "skipped"` (not charged).
- In monitoring mode each site in progress reserves the full $0.003 until its result is known. When less than that is left, a site already known to the list may still be re-checked for $0.0005; if it turns out to have changed, it is listed as `skipped` (not charged) and reported by your next run.
- **Never charged twice:** if the platform restarts or migrates a run, sites that were already audited are not audited or charged again.
- **Big lists:** the default run timeout is 1 hour, about 3,000 sites at default settings. For more, raise *Timeout* in the run options, or give the run 1 GB+ memory and raise *Parallel sites*.

### Limits & notes

- Only the **homepage** of each site is analysed (plus `robots.txt`, the sitemap, DNS and — only for contact emails — at most one linked contact page). It is a fast site-level health check, not a full-site crawler.
- JavaScript is not executed. Pages that are built entirely by JavaScript are flagged with `jsRendered: true`, and title / meta / H1 findings for them are downgraded to low severity because a browser may add them later.
- Only public pages can be audited — anything behind a login is flagged with `behindLogin: true` and skipped.
- Large sites often block automated requests (see bot protection above). Small-business and agency-client sites rarely do.
- The Actor does not use or scrape any third-party SEO tool — all data comes from the websites themselves and public DNS.

### FAQ

**Is this a bulk SSL checker (SSL certificate checker)?** Yes — every row has the certificate issuer, expiry date and `daysLeft`, so you can filter for certificates expiring in the next 30 days across hundreds of domains.

**Is it a website checker / homepage SEO audit tool?** Yes, at homepage level: for every reachable site it checks the on-page SEO basics (title length, missing or too-long meta description, H1, canonical link, image alt text, robots.txt, XML sitemap) plus HTTPS redirect, SSL, security headers and email DNS, and rolls them into one 0–100 health score. It does not crawl every page of a site — it is built for checking hundreds of websites quickly, not for a deep single-site SEO crawl.

**Is this a bulk SPF / DMARC checker?** Yes — `dns.spf`, `dns.dmarc` and `dns.dmarcPolicy` are reported for every domain, and missing SPF/DMARC or `p=none` are flagged as issues.

**Is this a security headers checker?** Yes — HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Permissions-Policy, with an A–F grade.

**Does it find contact emails? Is it an email extractor?** It returns the contact emails each website publishes itself (`contactEmails`, up to 10, `info@`/`contact@` first) and the page they were on (`contactPage`) — from the homepage and at most one linked contact page, at no extra cost. It does not guess emails, look them up elsewhere or scrape social media, so small sites that only have a contact form get `[]`. See [Contact emails](#contact-emails) for the responsible-use note.

**Does it measure page speed / Core Web Vitals like Lighthouse?** No. It reports server response time and page size only. For Lighthouse scores, run a Lighthouse Actor on the sites this audit flags.

**Can I schedule it?** Yes — use Apify Schedules to run it weekly or monthly and get alerted about expiring certificates. Turn on **Monitoring mode** so each run reports only what changed and unchanged sites cost $0.0005.

**Is this a website change monitor / SSL certificate expiry monitor?** Yes, in monitoring mode: every scheduled run compares each site with its previous result and lists what changed — status, issues, SSL expiry date and the "expires within 14 days" flag, security-headers grade, SPF/DMARC, robots.txt, sitemap and contact emails. It does not track page text or design changes.

**Why does an unchanged site still cost something?** It is fully re-checked (homepage, SSL, DNS, robots, sitemap) — that is what lets the Actor say it did not change. It just costs a sixth of a full audit.

**Can I use it from code / Make / Zapier / n8n?** Yes, through the Apify API and integrations; every run returns a normal dataset.

**Can Claude, ChatGPT or another AI agent use it?** Yes — through the Apify MCP server, and agents can pay per call with x402. See [For AI agents](#for-ai-agents-mcp-x402) for a minimal input and the fields to read.

**How much will 10,000 sites cost?** At most $30 (only sites that are actually audited are charged). Set *Max cost per run* to cap it, and raise the run timeout — see [Cost control](#cost-control-max-sites-and-max-cost).

**Why is `healthScore` empty for some rows?** The homepage could not be analysed: the site is unreachable, behind bot protection or behind a login. Those rows are free; `status` and `error` say why.

**What does an input line that is not a domain do?** It gets a row with `status: "invalid"` so you can see what was ignored — not charged.

**Is this a website uptime / broken homepage checker?** Partly — every run reports the HTTP status, redirects and response time of each homepage, and a down homepage is flagged as `HTTP_ERROR`. Schedule it for periodic checks; it is not a minute-by-minute uptime monitor.

**Found a bug or need another check?** Open an issue on the Actor's Issues tab — requests are welcome.

### Support & changelog

Issues opened on the **Issues** tab are answered within 2 business days.

- **0.1.13 (2026-10-12)** — **monitoring mode** (`monitorMode`, `monitorName`, `includeUnchanged`): each run reports only what changed since the previous run (`change`, `changes`), unchanged sites get a short row billed $0.0005; new *Monitoring: changes* dataset view.
- **0.1.12 (2026-10-12)** — new `contactEmails` and `contactPage` columns: the public contact emails each site lists on its homepage or its contact page (option *Collect public contact emails*, on by default, no extra cost).
- **0.1.11 (2026-10-12)** — new `status` and `charged` columns; invalid input lines and sites skipped by your budget now get their own row (free); budget reserved per site so a run never goes over *Max cost per run*; progress is saved so a restarted run never charges a site twice; API inputs are validated (max 50,000 sites, 50 parallel); every output column documented.
- **0.1.10 (2026-10-11)** — a homepage that is down (HTTP 5xx) is now reported as `HTTP_ERROR` instead of "bot protection"; firewall refusals (401/403/405/429) are labelled as such; `www.` and bare domains are deduplicated; browser-like request headers reduce false refusals on small-business hosts.
- **0.1.7** — security-headers grade F lowered to medium severity.
- **0.1.0 (2026-09-27)** — first release.

# Actor input Schema

## `websites` (type: `array`):

Domains or homepage URLs, one per line (e.g. "example.com" or "https://www.example.com"). Duplicates and www/non-www twins are removed. Bare domains start on http:// so the HTTP→HTTPS redirect can be verified. Up to 50,000 sites per run (default 1-hour run timeout ≈ 3,000 sites; raise Timeout in run options for more). Cost: $0.003 per audited site (about $3 per 1,000); unreachable, bot-protected and login-walled sites are free.

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

Look up A/AAAA, MX, NS, SPF, DMARC and CAA records for each domain (fills the dns.\* columns). No extra cost.

## `checkSitemap` (type: `boolean`):

Fetch robots.txt, detect 'Disallow: /' for all crawlers and find the XML sitemap with its URL count (fills the crawl.\* columns). No extra cost.

## `extractContacts` (type: `boolean`):

Find the contact email addresses the website itself publishes (mailto links and visible text on the homepage; if it lists none, ONE extra request to the contact/about page linked from it). Fills contactEmails (up to 10, role addresses like info@ first) and contactPage. Nothing is guessed, no third-party lookups, no social profiles, no phone numbers. No extra cost. You are responsible for contacting people lawfully (GDPR, CAN-SPAM, local law).

## `maxSites` (type: `integer`):

Audit at most this many sites from the list (0 = all, maximum 50,000). Cost cap: each audited site costs $0.003, so 100 sites ≈ $0.30, 1,000 ≈ $3, 10,000 ≈ $30. You can also set "Max cost per run" in run options; sites beyond that budget are listed as skipped and not charged.

## `monitorMode` (type: `boolean`):

For weekly / monthly scheduled runs of the same list. The Actor remembers each site's result in a key-value store in YOUR account ("website-health-audit-monitor") and on the next run tells you what changed. New or changed sites get the full row plus change = "new" / "changed" and a changes list (e.g. "SSL expiry date: 2026-10-26 → 2027-01-24", "issues: +NO_DMARC"); they cost $0.003 like a normal audit. Unchanged sites get a short row with status "unchanged" and cost only $0.0005. Blocked and unreachable sites stay free and keep their last remembered result.

## `monitorName` (type: `string`):

Name of the list to remember (letters, digits and dashes). Use a different name for each client or list you monitor, e.g. "client-acme". Default: "default".

## `includeUnchanged` (type: `boolean`):

Monitoring mode only: add a short row (status "unchanged", score, top issues, SSL days left) for sites that did not change, so you can see every site was checked. Turn off to get only new and changed sites in the dataset — unchanged sites are still checked and billed at $0.0005, and counted in the MONITOR_SUMMARY record of the run's key-value store.

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

How many websites to audit at the same time (1-50). Also limited by memory: about 1 parallel site per 48 MB, so 10 at the default 512 MB. Raise memory and this value together for big lists.

## `requestTimeoutSecs` (type: `integer`):

Give up on a request after this many seconds (3-60). A site that never answers is reported as unreachable and not charged.

## Actor input object example

```json
{
  "websites": [
    "apify.com",
    "wikipedia.org",
    "example.com"
  ],
  "checkDns": true,
  "checkSitemap": true,
  "extractContacts": true,
  "maxSites": 0,
  "monitorMode": false,
  "monitorName": "default",
  "includeUnchanged": true,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

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

No description

## `overview` (type: `string`):

No description

## `changes` (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 = {
    "websites": [
        "apify.com",
        "wikipedia.org",
        "example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("quanta1810/website-health-audit").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 = { "websites": [
        "apify.com",
        "wikipedia.org",
        "example.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("quanta1810/website-health-audit").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 '{
  "websites": [
    "apify.com",
    "wikipedia.org",
    "example.com"
  ]
}' |
apify call quanta1810/website-health-audit --silent --output-dataset

```

## MCP server setup

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

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/fGeNLPRc5MfRayVmc/builds/rJ87NKpUzoucrfgKG/openapi.json
