On-Page SEO Audit API: Titles, Meta, Headings, Schema
Pricing
from $3.00 / 1,000 page auditeds
On-Page SEO Audit API: Titles, Meta, Headings, Schema
On-page SEO audit for a list of URLs or a capped crawl of a site: title and meta lengths, H1 to H3 outline, canonical, robots and noindex, hreflang, Open Graph, JSON-LD types, image alt text, links, redirects, word count, a 0 to 100 score and a fix list. Plain HTTP, obeys robots.txt.
Pricing
from $3.00 / 1,000 page auditeds
Rating
0.0
(0)
Developer
Itay Ozer
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
17 hours ago
Last modified
Categories
Share
Send a list of page URLs, or one homepage plus a crawl limit, and get an on-page SEO audit for every page: title and meta description lengths, the H1 to H3 outline, canonical and noindex checks, hreflang, Open Graph, schema.org types from JSON-LD, image alt text, link counts, redirects, response time and word count. Each page gets a 0 to 100 score and a fix list written in plain language.
It reads pages over plain HTTP, the way a search engine crawler does on its first pass. No headless browser and no proxy, so it is fast (a 100-page crawl of a docs site took under 4 seconds in our tests) and cheap: $3 per 1,000 pages. Error pages, pages blocked by robots.txt and duplicates cost nothing.
It obeys each site's robots.txt (RFC 9309) and never reads pages behind a login.
What you get
One result per page:
score(0 to 100) andissues: every problem found, each with acode, aseverity(error,warningornotice), thepointsit takes off the score, and amessagethat says how to fix ittitle,titleLength,metaDescription,metaDescriptionLengthh1(every H1 text),h1Count, andheadings: the H1 to H3 outline in page ordercanonical,canonicalIsSelf,metaRobots,xRobotsTag(the response header) andindexable(false when noindex is set or the canonical points to another page)language(from<html lang>),viewport,hreflangalternatesopenGraph(allog:tags),twitterCard,jsonLdTypes(such asOrganization,Product,BreadcrumbList,FAQPage, including types nested in@graph)imagesTotalandimagesMissingAlt,linksInternal,linksExternal,linksNofollow,wordCountof visible textstatusCode,redirectChain(every hop),finalUrl,responseMs(time to first byte),htmlBytes,contentTypeurl,foundOn(for crawled pages: the page that links to it),status,error,fetchedAt
What the score checks
The score starts at 100 and loses the points of each issue found.
| Code | Severity | Points | Fires when |
|---|---|---|---|
title-missing | error | 15 | No <title> |
title-too-long / title-too-short | warning | 5 / 3 | Title over 60 or under 30 characters |
meta-description-missing | error | 10 | No meta description |
meta-description-too-long / -too-short | warning | 3 | Over 160 or under 70 characters |
h1-missing | error | 10 | No H1 |
h1-multiple | warning | 3 | More than one H1 |
heading-levels-skipped | warning | 2 | For example an H3 straight after an H1 |
noindex | error | 15 | noindex in the robots meta tag or X-Robots-Tag header |
canonical-missing | warning | 3 | No canonical link |
canonical-elsewhere | warning | 5 | The canonical points to another address |
viewport-missing | error | 8 | No viewport meta tag (not mobile friendly) |
lang-missing | warning | 3 | No lang on <html> |
not-https | error | 10 | The page is served over plain HTTP |
images-missing-alt | warning | 1 to 10 | Images without an alt attribute, scaled by their share |
needs-javascript | error | 15 | Almost no content without JavaScript |
thin-content | warning | 5 | Under 200 words of visible text |
redirect-chain / redirected | warning / notice | 3 / 1 | Two or more redirects / one redirect before the page |
slow-response | warning | 3 | Time to first byte over 1,800 ms |
json-ld-invalid | warning | 5 | A JSON-LD block that is not valid JSON |
structured-data-missing | notice | 2 | No JSON-LD at all |
open-graph-incomplete | notice | 2 | og:title, og:description or og:image missing |
Input
| Field | Required | Example | Notes |
|---|---|---|---|
urls | yes | ["https://www.python.org/"] | Full addresses, up to 10,000 per run. An address typed without https:// is read as https |
maxCrawlPages | no | 100 | 0 (default) audits only your URLs. A number makes it follow links to other pages on the same site and audit up to that many more pages per site, up to 5,000 |
Audit a list of pages:
{"urls": ["https://www.python.org/", "https://www.gov.uk/"]}
Audit a whole site, up to 500 pages:
{"urls": ["https://www.example-shop.com/"],"maxCrawlPages": 499}
While crawling it stays on the same site (www. and the bare domain count as one site), skips links marked nofollow, pages that are nofollow as a whole, files such as PDFs, images and archives, and anything robots.txt disallows. Links that lead to a page already audited in the run come back as free duplicate rows, and broken links come back as free http-error rows with foundOn telling you which page links to them.
Output example
A real result for python.org, shortened:
{"url": "https://www.python.org/","finalUrl": "https://www.python.org/","status": "ok","statusCode": 200,"redirectChain": [],"responseMs": 11,"score": 88,"issues": [{ "code": "title-too-short", "severity": "warning", "points": 3, "message": "The title is 21 characters. Use 30 to 60 characters to describe the page." },{ "code": "meta-description-too-short", "severity": "warning", "points": 3, "message": "The meta description is 52 characters. Use 70 to 160 to describe the page." },{ "code": "h1-multiple", "severity": "warning", "points": 3, "message": "The page has 5 H1 headings. Keep one H1 and use H2 for sections." },{ "code": "canonical-missing", "severity": "warning", "points": 3, "message": "No canonical link. Add <link rel=\"canonical\" href=\"...\"> with the preferred address of this page." }],"title": "Welcome to Python.org","titleLength": 21,"metaDescription": "The official home of the Python Programming Language","metaDescriptionLength": 52,"h1": ["Functions Defined", "All the Flow You’d Expect", "Compound Data Types", "Intuitive Interpretation", "Quick & Easy to Learn"],"h1Count": 5,"headings": [{ "level": 1, "text": "Functions Defined" }, { "level": 1, "text": "All the Flow You’d Expect" }],"canonical": null,"canonicalIsSelf": null,"metaRobots": null,"indexable": true,"language": "en","viewport": "width=device-width, initial-scale=1.0","hreflang": [],"openGraph": { "type": "website", "title": "Welcome to Python.org", "image": "https://www.python.org/static/opengraph-icon-200x200.png", "url": "https://www.python.org/" },"jsonLdTypes": ["WebSite", "SearchAction"],"imagesTotal": 1,"imagesMissingAlt": 0,"linksInternal": 126,"linksExternal": 79,"linksNofollow": 0,"wordCount": 1066,"fetchedAt": "2026-10-31T17:00:25.605Z"}
Every URL gets a row, so you always know what happened:
status | Meaning | Charged |
|---|---|---|
ok | HTML page audited | yes |
http-error | The page answered 4xx or 5xx (for example 404 or 403) | no |
not-html | A file (PDF, image, archive), not a web page | no |
blocked-by-robots | The site's robots.txt disallows it, or robots.txt had a server error | no |
too-many-redirects | More than 5 redirects | no |
duplicate | Crawl only: leads to a page already audited in this run | no |
fetch-failed | No answer (timeout, unknown host) | no |
invalid-url | Not a public http or https address | no |
Pricing
Pay per event:
| Event | Price | When |
|---|---|---|
| Page audited | $0.003 | Every result with status ok |
| Actor start | $0.00005 | Once per run |
Example: a 500-page site crawl where 470 pages are audited and 30 are broken links costs 470 x $0.003 + $0.00005 = $1.41005.
The actor checks your run's maximum charge before it fetches anything and stops at what that covers, so a run never goes over the limit you set.
Limits
- Pages are read without running JavaScript, like a crawler's first pass. Pages that only show content after JavaScript runs get the
needs-javascriptissue. There are no Core Web Vitals or Lighthouse scores. - Sites that block bots (a 403 page or a challenge wall) return
http-error. There is no proxy rotation or unblocking. - It does not check whether outgoing links work, except the same-site links it visits while crawling.
- HTML is read up to 4 MB per page. Up to 5 pages are fetched at a time. When a site answers 429 or 503 (too busy), it waits up to 10 seconds (following
Retry-After) and tries once more. - The score is a quick on-page health check, not a ranking prediction. Thresholds follow common SEO practice (titles up to about 60 characters, descriptions up to about 160).
FAQ
Can it audit a whole website?
Yes. Put the homepage in urls and set maxCrawlPages. It follows links between pages of the same site, breadth first, and stops at your limit or your maximum charge, whichever comes first.
Does it respect robots.txt?
Always, and it cannot be turned off. It identifies itself as SeoPageAuditBot with a link to this page, reads each site's robots.txt once per run and follows RFC 9309. Pages it may not read come back as free blocked-by-robots rows.
Why did my own site come back with http-error 403?
Your firewall or CDN blocks automated requests. Allow the SeoPageAuditBot user agent, or audit a staging copy. Nothing is charged for blocked pages.
Can I call it from an AI agent, MCP client, Make, Zapier or the API? Yes. The input is a list of URLs, every output field has a fixed type, and the fix list is plain text an agent can act on. It runs with limited permissions, so it cannot touch your other Apify data.
Is there personal data in the results? The results describe page structure (titles, headings, tags, counts). The actor targets no site by default and adds nothing of its own. You need the right to audit the pages you send.
Something looks wrong in an audit. What do I do? Open an issue on the Issues tab with the run link and the URL.