REST API Reference

All analysis endpoints are async — submit a job, poll for progress, then fetch the result. Every request needs a Bearer API key.

Base URL

https://api.on-page.ai

Endpoints

POST/v1/classify0.2 credits

Categorize a page or text into 1,091 semantic categories.

Provide at least one of text or url. languageHint is optional.

Request

{
  "text": "optional raw text (max 20,000 chars)",
  "url": "https://example.com/article",
  "languageHint": "en"
}

Response

{
  "job_id": "job_...",
  "status": "queued",
  "request_id": "req_...",
  "estimated": { "queue_position": 1 }
}
VALIDATION_ERRORURL_NOT_ALLOWEDINSUFFICIENT_CREDITSORG_ACTIVE_LIMIT_REACHEDORG_QUEUED_LIMIT_REACHED
POST/v1/scan2 credits

Run a standard SEO scan against top Google results for a keyword.

Takes 30s-2min. Use for most SEO optimization tasks. Reports include related-entity density scores that compare the analyzed URL's important-entity signal against the competitor cohort. `keyword` is normalized for whitespace and capped at 150 characters; `url` is capped at 2048 characters. `region` selects the Google SERP country (27 supported — see GET /v1/regions or the in-operation table in the OpenAPI reference). `market` is a deprecated alias for `region`; if you send it the response carries `Deprecation: true` + `Link` headers — please migrate. `compatibility_mode` (default `false`) is an advanced flag that runs the slower compatibility crawl path against every ranking competitor — significantly slower scans, but higher coverage when the SERP is dominated by sites with strong bot protection (Cloudflare, Akamai, etc.). The target URL already receives maximum-compatibility crawling automatically regardless of this flag.

Request

{
  "url": "https://example.com/page",
  "keyword": "target keyword",
  "region": "US",
  "locale": "en-US",
  "compatibility_mode": false
}

Response

{
  "job_id": "job_...",
  "status": "queued",
  "request_id": "req_...",
  "estimated": { "queue_position": 0 }
}
VALIDATION_ERRORREGION_VALIDATION_ERRORREGION_MISMATCHURL_NOT_ALLOWEDINSUFFICIENT_CREDITSORG_ACTIVE_LIMIT_REACHEDORG_QUEUED_LIMIT_REACHED
POST/v1/scan/lite1.5 credits

Quick SEO scan focused on entity coverage and competitor cohort analysis — ideal for fast audits and bulk workflows. Returns benchmarks + entities + highly related words + cohort analysis. Skips page classification, internal link recommendations, and topical-authority questions.

Same inputs as /v1/scan, including the optional `compatibility_mode` flag (default `false`; see /v1/scan notes). Result payload follows the customer_v1_lite shape (see /docs/report-schema) and includes related-entity density scores. Requesting response_format=customer_v1 on a Lite job is rejected with 409 UNSUPPORTED_FORMAT — use customer_v1_lite or omit the parameter.

Request

{
  "url": "https://example.com/page",
  "keyword": "target keyword",
  "region": "US",
  "locale": "en-US",
  "compatibility_mode": false
}

Response

{
  "job_id": "job_...",
  "status": "queued"
}
VALIDATION_ERRORURL_NOT_ALLOWEDINSUFFICIENT_CREDITSORG_ACTIVE_LIMIT_REACHEDORG_QUEUED_LIMIT_REACHED
POST/v1/scan/deep3 credits

50% deeper than Standard with 15 competitors. Deep responses may also include a SERP-speed benchmark — head-to-head loading speed and visual stability of your page vs the top 3 organic competitors in the same SERP.

Same inputs as /v1/scan, including the optional `compatibility_mode` flag (default `false`; see /v1/scan notes). Takes 1–3 min. The terminal report (GET /v1/jobs/:id/result) carries the standard customer_v1 sections, including related-entity density scores, and Deep responses MAY also include the optional `serp_speed_benchmark` field — page-experience metrics (LCP, CLS, approximate TBT, TTFB; FCP at per-probe level) for the target page and the top 3 organic competitor URLs, measured under identical lab conditions. Lite and Standard never include this field; clients must check for presence. Self-hosted; never calls Google PageSpeed Insights, CrUX, or Lighthouse. See the OpenAPI reference at /reference (`serp_speed_benchmark` schema) for the field shape and the per-probe `status` enum.

Request

{
  "url": "https://example.com/page",
  "keyword": "target keyword",
  "region": "US",
  "locale": "en-US",
  "compatibility_mode": false
}

Response

{
  "job_id": "job_...",
  "status": "queued"
}
VALIDATION_ERRORREGION_VALIDATION_ERRORREGION_MISMATCHURL_NOT_ALLOWEDINSUFFICIENT_CREDITSORG_ACTIVE_LIMIT_REACHEDORG_QUEUED_LIMIT_REACHED
GET/v1/jobs/:jobIdFree

Poll job status and progress.

Status values: "queued", "running", "completed", "failed". `region` + `locale` echo the canonical scan region (always `null` for classify jobs and for legacy scan rows that pre-date region support).

Request

No request body.

Response

{
  "job_id": "job_...",
  "type": "scan",
  "status": "running",
  "progress": 45,
  "created_at": "2026-04-14T...",
  "started_at": "2026-04-14T...",
  "completed_at": null,
  "region": "UK",
  "locale": "en-GB",
  "result": null,
  "error": null
}
INVALID_API_KEYNOT_FOUND
GET/v1/jobs/:jobId/resultFree

Fetch the full report for a completed job.

Returns the customer_v1 format by default. Lite and Standard responses never carry `serp_speed_benchmark`; Deep responses MAY include it (optional field — present when the benchmark produced a payload). Clients should check for presence on Deep results. See Report Schema docs for field details.

Request

No request body.

Response

{
  "schema_version": "onpage-report-customer-v1",
  "meta": { "report_date": "...", "target_keyword": "..." },
  "benchmarks": { "page1_average": {...}, "your_url": {...} },
  "entity_coverage": { ... },
  "topic_and_classification": { ... },
  "internal_linking": { ... },
  "competitor_term_coverage": { ... },
  "serp_speed_benchmark": { "status": "ok", ... }   // Deep only, optional
}
NOT_FOUNDRESULT_NOT_READY
GET/v1/creditsFree

Check available and reserved credit balances.

Use available_credits and reserved_credits for normal display. Reserved = credits held for in-progress jobs. The millicredit fields remain for existing integrations.

Request

No request body.

Response

{
  "available_credits": 9531,
  "reserved_credits": 48,
  "available_millicredits": 9531000,
  "reserved_millicredits": 48000
}
INVALID_API_KEY
GET/v1/regionsFree (public)

List supported scan regions for the `region` field.

27 supported regions. Cached for 24 hours (`Cache-Control: public, max-age=86400`). Use the `code` value as `region` on POST /v1/scan*. `googleDomain` is informational — crawls hit `google.com` with the `gl` parameter rather than the country-specific domain.

Request

No request body. No authentication required.

Response

{
  "defaultRegion": "US",
  "regions": [
    { "code": "US", "name": "United States", "flagEmoji": "🇺🇸",
      "googleDomain": "google.com", "googleGl": "us", "defaultLocale": "en-US" },
    { "code": "UK", "name": "United Kingdom", "flagEmoji": "🇬🇧",
      "googleDomain": "google.co.uk", "googleGl": "gb", "defaultLocale": "en-GB" }
    /* …25 more rows… */
  ]
}
POST/v1/webhooks/testFree

Queue a test webhook delivery to verify your endpoint.

The signing secret is returned so you can verify the signature.

Request

{
  "url": "https://your-app.example/webhooks",
  "event": "job.completed"
}

Response

{
  "delivery_id": "whd_...",
  "status": "queued",
  "signing_secret": "hex..."
}
VALIDATION_ERRORURL_NOT_ALLOWED

Code examples

curl

curl https://api.on-page.ai/v1/scan \
  -X POST \
  -H "Authorization: Bearer op_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","keyword":"example keyword","region":"US"}'

TypeScript

const res = await fetch("https://api.on-page.ai/v1/scan", {
  method: "POST",
  headers: {
    Authorization: "Bearer op_live_your_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com",
    keyword: "example keyword",
    region: "US",
  }),
});
const { job_id } = await res.json();

Python

import requests

resp = requests.post(
    "https://api.on-page.ai/v1/scan",
    headers={"Authorization": "Bearer op_live_your_key"},
    json={"url": "https://example.com", "keyword": "example keyword", "region": "US"},
)
print(resp.json()["job_id"])

Error codes

All errors return { "error": { "code": "...", "message": "..." } } with an appropriate HTTP status code.

CodeDescription
INVALID_API_KEYBearer token missing, malformed, or not valid.
VALIDATION_ERRORRequest body or query failed schema validation.
REGION_VALIDATION_ERRORUnknown region or market value. Response includes error.supported_regions listing the 27 valid codes (also at GET /v1/regions).
REGION_MISMATCHBoth `region` and `market` were sent with different values. Use `region` only — `market` is a deprecated alias.
INSUFFICIENT_CREDITSNot enough credits for this action.
RATE_LIMITEDRate limit exceeded. Check Retry-After header.
ORGANIZATION_SUSPENDEDOrganization suspended — contact support.
ORG_ACTIVE_LIMIT_REACHEDYour org already has 5 jobs running. Wait for one to finish.
ORG_QUEUED_LIMIT_REACHEDYour org has 100 jobs waiting. Wait for the queue to drain.
SCAN_QUEUE_SATURATEDGlobal scan capacity full. Retry later.
RESULT_NOT_READYJob hasn't produced a result yet.
URL_NOT_ALLOWEDURL failed safety checks.
NOT_FOUNDJob or resource doesn't exist.
IDEMPOTENCY_CONFLICTSame key reused with different payload.