Audit internal links
Every site accumulates pages that nobody links to anymore and pages that hoard all the link equity while giving none back. This endpoint crawls the links on a page or a submitted URL set and hands you a clean map of what points where, what points nowhere, and what never gets pointed to at all.
Run it online
Run this on our servers with your account. Free tools run in your browser; this one bills your KIT balance per the price above.
What actually breaks
Internal linking rots quietly. A category gets renamed, a campaign page goes live and never gets folded into the main navigation, an old blog post is deleted but its inbound links survive on twelve other pages. None of this throws a server error, so it never shows up in a normal uptime check — it just slowly starves parts of a site of crawl attention and ranking signal while a handful of pages absorb link equity they don't need.
What the audit returns
Submit a URL or a list of URLs and the task extracts every a href on the page, resolves relative paths, and classifies each link as internal, external, or self-referencing. Across a batch it builds a picture of orphan pages that exist in your sitemap but receive zero internal links, and link sinks — pages that accumulate large numbers of inbound links while sending very few outbound, which is often a sign of an unbalanced navigation or footer template rather than genuine authority.
Where this comes from
Internal link analysis has been part of technical SEO since PageRank first made link graphs a ranking input in the late 1990s, and the underlying logic hasn't changed: a page with no path leading to it is effectively invisible to a crawler, no matter how good the content is. What has changed is the scale of modern sites, where thousands of URLs get generated by templates and nobody manually verifies that each one is reachable.
Who runs this and how often
Technical SEOs auditing a migration, developers validating that a new template didn't quietly drop the related-articles module, and content teams doing a quarterly link-equity pass all use this the same way: point it at a sitemap-derived URL list on a schedule and diff the orphan count over time. Because each request is a few tenths of a cent, running it weekly against a full site catalog costs less than a single manual crawl tool license.
Fitting it into a pipeline
The endpoint is async by design — you get a task_id immediately and the full link graph lands on your webhook once the crawl finishes, so it slots into a nightly job or a post-deploy check without holding a connection open. No result is ever generated from a failed fetch, and nothing you submit is retained past the stated window or used to train anything.
What you can do with it
Post-migration audit
After moving a site to a new CMS, run the checker across the old sitemap to confirm every previously-linked page still has an internal path pointing to it.
Template regression check
Catch the moment a redesign silently removes the 'related posts' block by watching the orphan-page count spike right after deploy.
Editorial link equity review
Content teams identify cornerstone pages that receive almost no internal links and add contextual links from newer posts to boost them.
Navigation footer audit
Spot footer or sidebar templates that are quietly funneling most of a site's link equity into a handful of low-value utility pages.
FAQ
What does the internal link checker API actually crawl?
It parses the HTML of each submitted URL, extracts every anchor tag, resolves relative and absolute paths, and classifies each link as internal, external, or self-referencing.
Can it find orphan pages across a whole site?
Yes — submit a batch of URLs (typically from your sitemap) and the results show which of them receive zero internal inbound links.
Is there a free tier?
There's no free tier — free tiers get abused and slow everyone down. Access runs on a prepaid ForHosting KIT balance: top up from $10.00 (it never expires) and each request is charged at its published price, so a call with no balance returns HTTP 402. No subscription, no tokens, no invented credits, and a failed task is never charged.
How much does one request cost?
$0.002 per request, billed only on a successful crawl — a failed fetch after three retries is never charged.
How do I get the results?
By signed webhook (recommended for automation) or a signed link valid for 24 hours if you prefer to poll.
Does it follow links behind login or JavaScript rendering?
It parses the delivered HTML of the URL you submit; pages that require authentication or client-side rendering to expose their links should be submitted as pre-rendered HTML.
What counts as a link sink?
A page with a disproportionately high ratio of inbound to outbound internal links, often caused by a global template rather than editorial intent.
Can I run this on a schedule?
Yes, the endpoint is async and stateless per request, so it's commonly called on a cron or post-deploy hook without any special setup.
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/internal-links \
-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/internal-links", {
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/internal-links",
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/internal-links", 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/internal-links", 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.internal_links",
"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. |