Check heading structure
Screen readers and search engines both build a page's outline from its heading tags, and a broken sequence (an H3 that jumps straight from an H1, or three separate H1s fighting for the same slot) confuses both. The Heading Structure API pulls a URL's H1 through H6 tags, maps them into an outline, and flags exactly where the hierarchy breaks. It's a narrow, fast check built for the one thing a full audit tends to bury.
Run — free
Runs in your browser. Free, unlimited — your data never leaves this page.
A small tag with an outsized job
Heading tags do double duty as visual hierarchy for sighted readers and as a navigable outline for screen reader users and search engine parsers, which is why a heading structure that looks fine styled on the page can still be structurally broken underneath. A designer picking an H4 for its font size instead of its place in the outline is a common, invisible mistake that this check exists specifically to surface.
What the check returns
POST /seo/headings fetches the page and extracts every heading tag in document order, then reports the full outline alongside specific flags: no H1 present, more than one H1, a level skipped in the sequence such as an H2 followed directly by an H4, and headings that are empty or duplicate content elsewhere on the page. You get both the raw outline and the list of problems, so you can see the structure and the diagnosis in one response.
Where the H1-through-H6 convention came from
The heading levels trace back to the earliest days of HTML, when documents were expected to nest sections the way an outline or a table of contents would, long before CSS existed to make any tag look like anything you wanted. That original intent, a strict logical hierarchy independent of visual styling, is exactly what modern accessibility tools and search engines still rely on, which is why the old habit of choosing heading levels by how they render rather than where they sit in the outline keeps causing real problems.
A focused check, not a full audit
This endpoint does one thing rather than bundling headings into a broader scan, which makes it cheap and fast enough to run on every page in a build pipeline or content workflow without waiting on a heavier report. The call is asynchronous: you get a task_id immediately and the outline plus flags arrive by signed webhook or a 24-hour signed link, so it slots into automated checks the same way a linter would.
Using it as a guardrail, not a one-off fix
Run it on every page before publish, or across a template once and trust that every page built from it inherits a clean outline. No page content is retained past the retention window, and nothing checked here trains any model.
What you can do with it
Catching a missing H1 before publish
Gate a CMS publish step on this check so a page can't go live without exactly one H1 present.
Fixing accessibility complaints tied to navigation
Run the check on pages flagged by screen reader users as confusing to navigate, and locate the exact skipped heading level causing it.
Auditing a template used across thousands of pages
Check the rendered output of one representative page from a template rather than every page individually, since a structural flaw there repeats everywhere.
Reviewing headings after a CMS or theme migration
Re-check key pages after switching themes, since new markup sometimes silently changes which tag renders as which heading level.
FAQ
What counts as a broken heading structure?
Missing or multiple H1 tags, a heading level skipped in sequence such as H2 straight to H4, and empty headings are the main issues this check flags.
Is one H1 per page a hard rule?
It's the strong convention that both accessibility tools and search engines expect, and the check flags any page with zero or more than one H1 so you can decide how to fix it.
Does it check heading content quality, not just structure?
It focuses on structural correctness and empty or duplicated headings; deeper keyword or content analysis belongs to a separate audit rather than this focused check.
Is there a free way to test it first?
The tool above runs free in your browser. The API is paid — each call draws from your prepaid ForHosting KIT balance: top up from $10.00 (it never expires), pay each request's published price, and a call with no balance returns HTTP 402. No subscription, no tokens, and a failed task is never charged.
How do results come back?
POST /seo/headings returns a task_id immediately, and the outline with flags arrives via signed webhook or a signed link valid for 24 hours.
What does it cost to check a large site?
It's a flat $0.002 per request, so checking a thousand pages has a predictable, small total cost.
What happens if the page fails to load?
The task retries automatically up to three times before returning a clear error, and you're never billed for a failed task.
Can this replace a full accessibility audit?
No, it checks one specific structural signal thoroughly; a complete accessibility review covers far more than heading hierarchy.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, and soon from our app, email and Telegram.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/seo/headings \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://ejemplo.com"}'const res = await fetch("https://api.kit.forhosting.com/seo/headings", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"url": "https://ejemplo.com"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/seo/headings",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"url": "https://ejemplo.com"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/seo/headings", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"url":"https://ejemplo.com"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"url":"https://ejemplo.com"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/seo/headings", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"url": "https://ejemplo.com"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "seo.headings",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |