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.

cURL · JavaScript · PythonStandard scan · 2 credits
01 · Submit a scanPOST /v1/scan
02 · Wait for completionGET /v1/jobs/:id
03 · Read the candidatesGET /v1/jobs/:id/result

01 / 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.

Terminal setup
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

POST /v1/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

GET /v1/jobs/:id
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

GET /v1/jobs/:id/result
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.

GET /v1/jobs/:id/result · Selected fields
{
  "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:

One item in candidates
{
  "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.

CMS filter · JavaScript
// 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

Application review of the candidate source pages
SourceDecisionReason
/ai-and-copywriting/ ↗KeepAn article with an unlinked phrase about optimizing content for search engines.
/ai-writing-tools/ ↗KeepAn article with an unlinked phrase about optimizing writing for SEO.
/category/on-page/ ↗SkipA 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 preview

Original

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
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.

Bring internal linking into your application

With On-Page.ai handling candidate discovery, you can focus on the editing experience: reviewing suggestions, approving links, and updating content. You now have the API workflow, a CMS filter, and an example of how to present a suggested link.

Replace the example URL and keyword, connect your CMS, and try the workflow on one article.

Building in another language? Use the same HTTP endpoints above, or explore client generation from our OpenAPI specification.