Developer tutorial · REST API
Build an internal
linking tool.
Give your app a target page. Get back pages that could link to it. Use the REST API from your preferred language, then turn the results into a suggested-links feature in your CMS or SEO dashboard.
POST /v1/scanGET /v1/jobs/:idGET /v1/jobs/:id/result01 / Working example
Run the example
Choose cURL, JavaScript, or Python and use your API key. Keep the key on your server.
We’ll use our SEO copywriting article as the target and look for related posts that could link to it. The scan uses the keyword seo copywriting and region US. Replace the URL, keyword, and region to use your own page.
Run these commands in Bash or zsh with cURL 7.76+ and OpenSSL. The same HTTP requests work in PHP, Go, Java, C#, or any other language with an HTTP client.
export ONPAGE_API_KEY="your-api-key"
# Generate once for this scan; keep the same key when retrying.
export ONPAGE_IDEMPOTENCY_KEY="$(openssl rand -hex 16)"1. Submit a scan
curl --include --fail-with-body --silent --show-error --max-time 30 \
-X POST "https://api.on-page.ai/v1/scan" \
-H "Authorization: Bearer $ONPAGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $ONPAGE_IDEMPOTENCY_KEY" \
--data '{
"url": "https://blog.on-page.ai/seo-copywriting/",
"keyword": "seo copywriting",
"region": "US"
}'Copy job_id from the response into the next command. Keep the same idempotency key and request body if you need to retry submission; keys are retained for 24 hours.
2. Check the job status
export ONPAGE_JOB_ID="job_id_from_the_scan_response"
curl --include --fail-with-body --silent --show-error --max-time 30 \
"https://api.on-page.ai/v1/jobs/$ONPAGE_JOB_ID" \
-H "Authorization: Bearer $ONPAGE_API_KEY"Repeat the GET request every few seconds until status is completed. Stop on failed or cancelled. To resume an interrupted workflow, reuse the job ID.
3. Retrieve the report
curl --include --fail-with-body --silent --show-error --max-time 30 \
"https://api.on-page.ai/v1/jobs/$ONPAGE_JOB_ID/result?response_format=customer_v1" \
-H "Authorization: Bearer $ONPAGE_API_KEY"Read internal_linking from the returned JSON. The JavaScript and Python versions automate polling and turn those URLs into application data.
02 / Response contract
Read the response
The scan submission returns job_id. Use it to poll the status endpoint until status is completed, then fetch the result. The status endpoint’s result field stays null.
Here’s the internal-linking section for our SEO copywriting article. Read report.internal_linking from the result endpoint; the fields are at the top level. The example JSON also gives you a test fixture to use while building your UI.
{
"jobId": "job_d814dbab-2a93-4894-be77-dda73b15595a",
"schema_version": "onpage-report-customer-v1",
"internal_linking": {
"add_internal_links_from": [
"https://blog.on-page.ai/ai-and-copywriting/",
"https://blog.on-page.ai/category/on-page/",
"https://blog.on-page.ai/ai-writing-tools/"
],
"to_your_url": "https://blog.on-page.ai/seo-copywriting/"
}
}add_internal_links_fromstring[]- Candidate source pages where your app could suggest a link to the target. Inspect their content to find a suitable placement.
to_your_urlstring- The destination for the suggested links. In this example, it’s our SEO copywriting article.
jobIdstring- The scan’s identifier. Store it with the suggestions for debugging.
On-Page.ai supplies the candidate URLs. Your application reads the page content, checks existing links, and chooses anchor text. This lets you bring the suggestions into your own editing workflow.
Internal linking is available in Standard and Deep scans. Lite scans omit this section.
Turn the response into application data
The JavaScript and Python examples print an object with jobId, targetUrl, and a candidates array. Each item has this shape. The examples add status so your app can track which suggestions still need review:
{
"sourceUrl": "https://blog.on-page.ai/ai-and-copywriting/",
"targetUrl": "https://blog.on-page.ai/seo-copywriting/",
"status": "needs_review"
}Store this data against the target page in your database. Use the source and target’s canonical URLs as a unique pair so processing the same report again won’t create duplicate suggestions.
03 / Application integration
Build the “suggested links” feature
Join the candidates to your CMS’s published articles. Your CMS adapter should return each article’s canonicalUrl and anoutgoingLinks array. Before calling the helper, resolve candidate URLs, article URLs, and outgoing links to their canonical URLs in your adapter, including relative links and redirects.
// articles: published articles from your CMS.
// The adapter resolves their URLs and outgoing links to canonical URLs.
function buildReviewQueue(candidates, articles) {
const byUrl = new Map(
articles.map((article) => [article.canonicalUrl, article]),
);
return candidates.filter(({ sourceUrl, targetUrl }) => {
const article = byUrl.get(sourceUrl);
if (!article) return false; // Archives aren't in the articles collection.
return !article.outgoingLinks.includes(targetUrl);
});
}This keeps editable articles that don’t already link to the target. Let’s walk through the three pages returned for our example. Two are articles with relevant passages; the third is a category archive that already links to the target in its listing.
Three candidates → two articles to review
| Source | Decision | Reason |
|---|---|---|
| /ai-and-copywriting/ ↗ | Keep | An article with an unlinked phrase about optimizing content for search engines. |
| /ai-writing-tools/ ↗ | Keep | An article with an unlinked phrase about optimizing writing for SEO. |
| /category/on-page/ ↗ | Skip | A category archive that already links to the target through its article listing. |
Add a link suggestion to your editor
Read each remaining article’s body from your CMS and find a relevant, unlinked phrase. The AI copywriting article already mentions optimizing content for search engines, a natural place to link to the SEO copywriting guide. Show the original text and proposed change together so the editor can review the placement:
/ai-and-copywriting/Editor previewOriginal
Another key advantage of AI copywriting is its ability to optimize content for search engines.
Suggested change
Another key advantage of AI copywriting is its ability to optimize content for search engines.
Suggested HTML
Another key advantage of AI copywriting is its ability to <a href="https://blog.on-page.ai/seo-copywriting/">optimize content for search engines</a>.The AI writing tools article offers another placement: “optimizing your writing for SEO”. Both suggestions connect an existing phrase to a guide that develops the topic, giving readers a useful next step.
If you use an LLM to select the phrase, give it the actual paragraph and target page content. Have it return a paragraph ID and exact anchor text. Your code should confirm that the phrase still exists outside a link, then add the link through your CMS’s editor API. Recheck the content revision before saving so a suggestion can’t overwrite a newer edit.
04 / Error handling
Handle job and result states
Pending job
Store the job ID and show a pending state in your UI. The JavaScript and Python examples poll every two seconds and stop waiting after five minutes. For a deployed app, use a background worker or job webhooks to retrieve the result when it’s ready.
HTTP errors and timeouts
The JavaScript and Python examples retry 429 and 503 responses with a numeric Retry-After header. Other HTTP errors and request timeouts stop execution. Resume with the saved job ID, or retry submission with the same input and idempotency key. Generate a new key for an intentional new scan. With cURL, inspect error responses and retry manually, honoring the Retry-After header. See rate limits and error codes.
Failed or cancelled job
Stop polling. The JavaScript and Python examples throw an error containing the job ID, status, and API error.
No candidates
An empty array is a valid result. Show an empty state such as “No internal-link candidates found for this page.” If the entire internal_linking section is missing, check the scan tier and report format.
Sources outside your CMS
Skip pages your CMS can’t retrieve. If your app spans multiple hostnames, adjust the examples’ origin filter to match your connected sites before resolving candidates to content records.