Validate hreflang
Hreflang is the one SEO tag that fails almost silently and almost always in pairs: get one direction right and the reciprocal wrong, and search engines can ignore the whole cluster.
Run — free
This endpoint reads a page and lists every hreflang alternate it declares — the language code and the URL each points to — and tells you whether an x-default is present, so you can review the block a page emits.
Why hreflang breaks so often
Hreflang annotations require every language or region version of a page to list every other version, including itself, and every one of those links has to be reciprocated exactly — if the Spanish page lists English as an alternate, the English page must list Spanish back. Miss one direction and you don't get a partial result; search engines documented early on that they treat an unreciprocated hreflang pair as unreliable and tend to disregard it, which quietly undoes the entire point of adding the tags.
What the reader returns
For a submitted page, the task collects every hreflang alternate declared, listing each language-region code and the URL it points to, reports how many there are, and flags whether an x-default alternate is present — the fallback search engines expect on pages meant to serve locales that match no specific language. It reads the tags a single page declares; it does not fetch the target pages, so confirming that each link is reciprocated means reading the tags on those pages too and comparing the lists.
A tag with a specific, narrow job
Hreflang was introduced in 2011 to solve one problem: telling a search engine which version of near-duplicate content to serve a given language and region, without that engine mistaking the versions for plain duplicate content. It doesn't influence rankings or translate anything by itself — it's purely a routing signal, which is exactly why a broken reciprocal link is so damaging: the signal simply stops being trusted.
Where it usually breaks in practice
The most common failures come from partial rollouts, where a new region launches with hreflang added only on the new pages and never backfilled onto the existing ones, and from CMS templates that hardcode a language list that quietly goes stale the moment a market is added or retired. Reading the block each template emits, page by page, is how you catch a language that was dropped or a code that was mistyped before it costs you traffic.
Running it continuously
Because international sites tend to have the most pages and the most churn, reading the hreflang block after every deploy is a cheap habit: submit a URL, get a task_id back immediately, and receive the list of every hreflang tag the page declares on your webhook once the fetch finishes. At $0.002 per request, checking a page's hreflang block after every content push costs a fraction of the ranking loss one broken pair can cause.
What you can do with it
New market launch check
List every hreflang alternate a new regional page declares the moment it goes live, and confirm the codes and the x-default you expect are all present before search engines index it.
Template audit after a deploy
Read the hreflang block a page's head emits after a release and confirm the language list is complete, a common place for a market to silently drop out.
x-default audit
Verify that pages meant to serve as the fallback for unmatched locales correctly declare x-default rather than defaulting to a specific language by accident.
Post-CMS-migration validation
After switching platforms, confirm the new templates still emit the full hreflang alternate list you expect on each page.
FAQ
What does the hreflang reader return?
It lists every hreflang alternate a page declares — each language-region code and its target URL — reports the count, and flags whether an x-default is present.
Why does hreflang need to be reciprocal?
Search engines generally disregard an hreflang pair if only one page in the pair declares it, so a missing return link can invalidate the whole cluster, not just one page — which is why reading the block on each page and comparing the lists matters.
Can it check a whole language cluster in one request?
It reads one page per request and lists the alternates that page declares. To review a whole cluster, run each page and compare the lists — the reader does not fetch the linked pages for you.
Does it validate the hreflang codes themselves?
No. It extracts and lists the codes exactly as the page declares them, so you can spot typos and odd combinations, but it does not check them against the ISO 639-1 and ISO 3166-1 tables.
Is there a free tier for this endpoint?
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 much does each check cost?
$0.002 per request, and a failed fetch is retried up to three times before returning an error at no charge.
How do I receive the results?
By signed webhook for automated pipelines, or a signed link valid for 24 hours if you'd rather retrieve it manually.
Does it read hreflang declared in a sitemap as well as in HTML?
No. It reads the hreflang tags in a page's HTML. Hreflang declared in a sitemap is validated with the sitemap tool instead.
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, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/seo/hreflang-check \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'const res = await fetch("https://api.kit.forhosting.com/seo/hreflang-check", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"url": "https://example.com"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/seo/hreflang-check",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"url": "https://example.com"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/seo/hreflang-check", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"url":"https://example.com"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"url":"https://example.com"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/seo/hreflang-check", 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://example.com"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "seo.hreflang_check",
"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. |