# SEO Audit API — On-Page Checks, Meta Tags, Headings & Schema (`highbrow_fame/seo-audit-api`) Actor

On-page SEO audit of any URL over plain HTTP: title, meta, canonical, robots, hreflang, headings, images alt, links, JSON-LD, redirects, a 0-100 score with issues. SEO Audit API.

- **URL**: https://apify.com/highbrow_fame/seo-audit-api.md
- **Developed by:** [yestrue](https://apify.com/highbrow_fame) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 pages

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

## SEO Audit API — On-Page Checks, Meta Tags, Headings & Schema

Give it a list of page addresses and get one clean JSON record per page with everything an on-page SEO check looks at: the redirect chain and final URL, HTTP status, HTTPS and HSTS, title and meta description with their lengths, robots directives (meta and X-Robots-Tag), canonical link and whether it points to the page itself, hreflang set, viewport, language, favicon, H1-H6 counts and texts, Open Graph and Twitter card tags, every JSON-LD type on the page, word count and text-to-HTML ratio, images without alt text, internal/external/nofollow link counts, scripts, stylesheets, iframes and mixed content — plus a 0-100 score with a list of issues, each with a severity, a penalty and a plain-language message. Everything is read over plain HTTP the way a crawler reads it: no browser, no Lighthouse, so a thousand pages take minutes, not hours.

**Try it in 1 minute:**

1. Press **Start** — the prefilled input audits two pages: Wikipedia's article on search engine optimization and example.com.
2. Seconds later the **Output** tab shows both records with their score, issues, title, meta description, headings, canonical, structured data types and link counts.
3. Download JSON, CSV or Excel, or paste your own **URLs**, switch on **Links list** / **Images list**, or audit as a **mobile** user agent.

Price: **$1.00 per 1,000 pages** (one page = one record). Pages that answer 404/410, non-HTML answers, pages a robots.txt forbids and inputs that are not addresses are free.

**Use it to:**

- Audit a sitemap or a list of landing pages on a schedule and alert on score drops, new noindex tags, lost canonicals or broken redirect chains.
- Check a competitor's pages: title and description patterns, heading structure, schema types, word counts, internal linking.
- Feed an AI agent or a reporting pipeline with structured on-page facts instead of raw HTML.
- Verify a site migration: every old URL's status chain, final URL, canonical and hreflang in one table.
- Find pages with missing alt text, mixed content, thin content or missing Open Graph tags across a whole site.

### How it works

1. **Addresses** are normalised (https:// is added when missing, #fragments dropped) and deduplicated; one that is not a web address gets a free `INVALID_INPUT` record.
2. **robots.txt** of the page's site is read once per origin (default on). A page it disallows for this Actor gets a free `ROBOTS_DISALLOWED` record and is not fetched.
3. **The page** is requested over plain HTTP with a desktop Chrome or a mobile Safari user agent, following up to 10 redirects one hop at a time so every hop lands in `statusChain[]`. 404/410 → free `NOT_FOUND`; a non-HTML answer (PDF, JSON, image…) → free `NOT_HTML`; a page bigger than **Max page size** → free `FAILED`. Other 4xx/5xx answers and network errors are tried again (a 429/403 after a wait on the same route, a 5xx or a dropped connection through the proxy) and end as a free `FAILED` record with the reason.
4. **The HTML** is parsed (no JavaScript is run): meta tags, links, headings, images, scripts, JSON-LD (including `@graph` and nested objects), visible text.
5. **The score** starts at 100 and loses points per issue found; the rules and penalties are listed under Output. HTTP errors never reach the score — they are their own statuses.

Requests go directly from Apify's servers; the proxy is used only to try again after a network or server error.

### Input

| Field | What it does |
|---|---|
| `urls` | Pages to audit, one per line (https:// is added when missing). |
| `includeLinks` | Add `links[]` — `{ url, text, rel, internal }`, up to 500 (default off). The counts are always there. |
| `includeImages` | Add `images[]` — `{ src, alt, width, height }`, up to 200 (default off). |
| `includeHeadings` | Add `headings[]` — `{ level, text }` in page order, up to 200 (default on). |
| `includeSchema` | Add `schema[]` — the JSON-LD blocks as parsed objects, first 50 KB (default on). `jsonLdTypes[]` is always there. |
| `respectRobots` | Skip pages the site's robots.txt disallows (default on). |
| `userAgent` | `desktop` (Chrome on Windows, default) or `mobile` (Safari on iPhone). |
| `maxBytes` | Pages with bigger HTML are not read (default 3 MB). |
| `maxConcurrency` | Pages fetched at the same time (default 5). |
| `proxyConfiguration` | Used only to try again after a network or server error; the first try is direct. |

### Output

One record per page:

| Field | What it is |
|---|---|
| `url`, `httpStatus`, `statusChain[]`, `redirectCount` | The final address, its status, every hop `{ url, status }` and how many redirects were followed |
| `contentType`, `pageSizeBytes`, `responseTimeMs`, `isHttps`, `hsts`, `xRobotsTag` | Fetch facts (size after decompression; time including redirects and download) |
| `title`, `titleLength`, `metaDescription`, `metaDescriptionLength` | The texts and their lengths in characters |
| `metaRobots`, `noindex`, `nofollow` | The robots meta tag, and whether the meta tags or the X-Robots-Tag header say noindex / nofollow |
| `canonical`, `canonicalIsSelf`, `hreflang[]`, `hreflangHasXDefault` | Canonical link (absolute) and whether it is the page itself; `{ lang, href }` alternates |
| `lang`, `charset`, `hasViewport`, `viewportContent`, `favicon` | Document facts |
| `h1[]`, `h1Count` … `h6Count`, `headings[]` | Heading texts and counts |
| `og`, `twitter` | `{ title, description, image, type, url, siteName }` and `{ card, title, description, image }`; null when the page has none |
| `jsonLdTypes[]`, `hasStructuredData`, `schema[]` | Every `@type` found (also inside `@graph` and nested objects); JSON-LD, microdata or RDFa present; the parsed JSON-LD |
| `wordCount`, `textToHtmlRatio` | Visible words (scripts and styles stripped) and text length ÷ HTML length |
| `imagesTotal`, `imagesWithoutAlt`, `imagesWithoutAltSrcs[]`, `images[]` | Image counts, the sources without alt text (up to 50), the list |
| `internalLinksCount`, `externalLinksCount`, `nofollowLinksCount`, `links[]` | Links classified against the final URL — `www.` variants are the same site, other subdomains are external |
| `hasAmpLink`, `hasRssFeed`, `inlineScriptsCount`, `externalScriptsCount`, `stylesheetsCount`, `iframesCount`, `mixedContent` | Resources; `mixedContent` counts http:// resources on an HTTPS page |
| `score`, `issues[]`, `issuesCount` | 0-100, `{ id, severity, penalty, message }` and `{ error, warning, notice }` |
| `input`, `status`, `error`, `scrapedAt` | What you gave, `OK` / `NOT_FOUND` / `NOT_HTML` / `ROBOTS_DISALLOWED` / `INVALID_INPUT` / `FAILED`, the reason, and when |

The rules (score = 100 − penalties, never below 0):

| Issue | Severity | Penalty |
|---|---|---|
| `missing-title` / `title-too-long` (> 60) / `title-too-short` (< 20) | error / warning / notice | 15 / 5 / 2 |
| `missing-meta-description` / `meta-description-too-long` (> 160) / `meta-description-too-short` (< 70) | error / warning / notice | 10 / 3 / 2 |
| `missing-h1` / `multiple-h1` | error / warning | 10 / 5 |
| `missing-canonical` / `canonical-elsewhere` | warning / notice | 5 / 2 |
| `noindex` | error | 20 |
| `missing-viewport` / `missing-lang` | error / notice | 10 / 2 |
| `images-without-alt` | warning | 1 per image, at most 10 |
| `no-structured-data` / `no-open-graph` | notice | 3 / 3 |
| `not-https` / `mixed-content` | error / warning | 10 / 5 |
| `redirect-chain` (2 or more redirects) / `page-too-big` (> 2 MB) | notice / warning | 2 / 5 |
| `thin-content` (< 300 words) / `low-text-ratio` (< 10 %) | notice | 3 / 2 |
| `hreflang-no-x-default` | notice | 1 |

#### Example

```json
{
  "input": "example.com/guide/ranking",
  "url": "https://www.example.com/guide/ranking/",
  "httpStatus": 200,
  "statusChain": [
    { "url": "https://example.com/guide/ranking", "status": 301 },
    { "url": "https://www.example.com/guide/ranking/", "status": 200 }
  ],
  "redirectCount": 1,
  "contentType": "text/html; charset=utf-8",
  "pageSizeBytes": 6505,
  "responseTimeMs": 212,
  "isHttps": true,
  "hsts": true,
  "xRobotsTag": null,
  "title": "How Search Engines Rank Pages — A Field Guide",
  "titleLength": 45,
  "metaDescription": "A practical guide to how search engines crawl, index and rank web pages, with the on-page signals that matter most and how to check them.",
  "metaDescriptionLength": 137,
  "metaRobots": "index, follow, max-snippet:-1",
  "noindex": false,
  "nofollow": false,
  "canonical": "https://www.example.com/guide/ranking/",
  "canonicalIsSelf": true,
  "hreflang": [
    { "lang": "en", "href": "https://www.example.com/guide/ranking/" },
    { "lang": "de", "href": "https://www.example.com/de/guide/ranking/" },
    { "lang": "x-default", "href": "https://www.example.com/guide/ranking/" }
  ],
  "hreflangHasXDefault": true,
  "lang": "en",
  "charset": "utf-8",
  "hasViewport": true,
  "viewportContent": "width=device-width, initial-scale=1",
  "favicon": "https://www.example.com/static/favicon.ico",
  "h1": ["How search engines rank pages"],
  "h1Count": 1,
  "h2Count": 3,
  "h3Count": 2,
  "h4Count": 0,
  "h5Count": 0,
  "h6Count": 0,
  "headings": [
    { "level": 1, "text": "How search engines rank pages" },
    { "level": 2, "text": "Crawling and indexing" },
    { "level": 2, "text": "Ranking signals on the page" }
  ],
  "og": { "title": "How Search Engines Rank Pages", "description": "A practical field guide to ranking signals.", "image": "https://www.example.com/static/ranking-cover.png", "type": "article", "url": "https://www.example.com/guide/ranking/", "siteName": "Example Guides" },
  "twitter": { "card": "summary_large_image", "title": "How Search Engines Rank Pages", "description": "A practical field guide to ranking signals.", "image": "https://www.example.com/static/ranking-cover.png" },
  "jsonLdTypes": ["Article", "Organization"],
  "hasStructuredData": true,
  "schema": [{ "@context": "https://schema.org", "@type": "Article", "headline": "How Search Engines Rank Pages", "author": { "@type": "Organization", "name": "Example Guides" }, "datePublished": "2026-09-01" }],
  "wordCount": 536,
  "textToHtmlRatio": 0.47,
  "imagesTotal": 2,
  "imagesWithoutAlt": 0,
  "imagesWithoutAltSrcs": [],
  "images": [],
  "internalLinksCount": 6,
  "externalLinksCount": 3,
  "nofollowLinksCount": 2,
  "links": [],
  "hasAmpLink": true,
  "hasRssFeed": true,
  "inlineScriptsCount": 1,
  "externalScriptsCount": 1,
  "stylesheetsCount": 1,
  "iframesCount": 0,
  "mixedContent": 0,
  "score": 100,
  "issues": [],
  "issuesCount": { "error": 0, "warning": 0, "notice": 0 },
  "status": "OK",
  "error": null,
  "scrapedAt": "2026-10-19T12:00:00.000Z"
}
```

A page with problems looks like this in `issues[]`:

```json
[
  { "id": "missing-meta-description", "severity": "error", "penalty": 10, "message": "The page has no meta description." },
  { "id": "multiple-h1", "severity": "warning", "penalty": 5, "message": "The page has 3 H1 headings; use one." },
  { "id": "images-without-alt", "severity": "warning", "penalty": 3, "message": "3 image(s) have no alt text." },
  { "id": "thin-content", "severity": "notice", "penalty": 3, "message": "The page has only 15 words of visible text; aim for 300 or more." }
]
```

### FAQ

**Is this an official API?** No, it's an unofficial API built on public pages — it reads any web page the way a search engine's crawler does and turns what it finds into a stable JSON record. There is no SEO authority behind the score: it is a transparent checklist with fixed penalties, listed above, so two runs on the same HTML always give the same number.

**Does it measure Core Web Vitals or Lighthouse scores?** No. This Actor reads HTML and headers over plain HTTP; it does not run a browser, so it cannot measure rendering, loading speed or layout shifts. For Lighthouse performance, accessibility, best-practice and SEO scores and the Core Web Vitals of the same URLs, use our **PageSpeed Insights API** Actor (`pagespeed-api`) — the two together cover both the content and the speed side of an audit.

**What about pages built with JavaScript?** The record shows what the server sends, which is also what a crawler sees first. A page that renders its title, headings or links only in the browser will look thin here; that is a real finding (search engines index server-sent HTML first and render later), but check such pages with a browser-based tool too.

**Why is a page NOT_FOUND, NOT_HTML or ROBOTS_DISALLOWED?** The address answers 404/410 (or its host does not resolve); the answer is not an HTML page (a PDF, an image, JSON); or the site's robots.txt disallows the page for this Actor's user agent — turn **Respect robots.txt** off only for sites you are allowed to read regardless. None of these is charged.

**Can it crawl a whole site?** Not in this version: it audits the addresses you give it. Paste the URLs of a sitemap, or combine it with a sitemap or crawler Actor whose output feeds this one.

**Like it?** A short review on the Store page helps other people find this Actor. Something missing or broken? Tell us on the Issues tab — we read every one.

### Related Actors

- [Sitemap Extractor](https://apify.com/highbrow_fame/sitemap-url-extractor) — every URL, date and image from a site's sitemaps
- [Website Tech Stack Detector](https://apify.com/highbrow_fame/website-tech-stack) — CMS, frameworks and analytics of any site
- [Domain Checker](https://apify.com/highbrow_fame/domain-whois-dns-ssl) — WHOIS, DNS and SSL expiry in bulk
- [Web Fetch API](https://apify.com/highbrow_fame/web-fetch-api) — any URL as HTML, text, Markdown or JSON
- [Website Screenshot API](https://apify.com/highbrow_fame/website-screenshot-api) — full page and mobile screenshots

# Actor input Schema

## `urls` (type: `array`):

Pages to audit, one per line (https:// is added when missing). Each gets one record with the fetch facts, the on-page checks and a 0-100 score with its issues.

## `includeLinks` (type: `boolean`):

Add links\[] — every link on the page as { url, text, rel, internal }, up to 500. The internal/external/nofollow counts are always there.

## `includeImages` (type: `boolean`):

Add images\[] — every <img> as { src, alt, width, height }, up to 200. The image counts and the sources of images without alt text are always there.

## `includeHeadings` (type: `boolean`):

Add headings\[] — every H1-H6 in page order as { level, text }, up to 200. The H1 texts and the per-level counts are always there.

## `includeSchema` (type: `boolean`):

Add schema\[] — the page's JSON-LD blocks as parsed objects (first 50 KB). Off, the record still lists every @type found in jsonLdTypes\[].

## `respectRobots` (type: `boolean`):

Read the site's robots.txt first and skip a page it disallows (a free ROBOTS_DISALLOWED record). Turn off only for sites you may read regardless.

## `userAgent` (type: `string`):

Request the page as a desktop Chrome or as a mobile Safari (iPhone). Sites that serve different HTML to phones can be audited both ways in two runs.

## `maxBytes` (type: `integer`):

A page whose HTML is bigger than this is not read (a free FAILED record). Default 3 MB.

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

How many pages to fetch at the same time. Lower it for a single small site; raise it for a list of many different sites.

## `proxyConfiguration` (type: `object`):

Used only to try again after a network error or a server error: the first try of every page goes out without a proxy.

## Actor input object example

```json
{
  "urls": [
    "https://en.wikipedia.org/wiki/Search_engine_optimization",
    "https://example.com"
  ],
  "includeLinks": false,
  "includeImages": false,
  "includeHeadings": true,
  "includeSchema": true,
  "respectRobots": true,
  "userAgent": "desktop",
  "maxBytes": 3145728,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Every result as a JSON record, with a status for each input.

## `resultsCsv` (type: `string`):

The same records as CSV, for a spreadsheet.

# 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 = {
    "urls": [
        "https://en.wikipedia.org/wiki/Search_engine_optimization",
        "https://example.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("highbrow_fame/seo-audit-api").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 = {
    "urls": [
        "https://en.wikipedia.org/wiki/Search_engine_optimization",
        "https://example.com",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("highbrow_fame/seo-audit-api").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 '{
  "urls": [
    "https://en.wikipedia.org/wiki/Search_engine_optimization",
    "https://example.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call highbrow_fame/seo-audit-api --silent --output-dataset

```

## MCP server setup

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

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/HM9yMtUTDW5E9jard/builds/OYGcNfP4iuaxHzAar/openapi.json
