NEWAI content checker: see which pages read like AI text

APISEO auditSEO audit API

SEO audit API. Put our site crawl, page checks and MCP tools into your own dashboard or AI.

Build your own client portal or report in a few afternoons, and we keep the crawlers running behind it. Everything the MarketingTracer SEO audit shows and does is in the REST API and the MCP server. It is included in every plan at no extra cost.

The SEO MCP with SEO Superpowers: the issue from the scan plus the fix, step by step, for a Shopify shop

Full site audit

Audit scores, failing rules with Search Console clicks, folder breakdowns and every scanned page.

Read and write

Start a site crawl, add or delete pages, save page notes or hide a rule over REST.

AI checks

See which AI crawlers can reach your site and which pages read like stock AI writing.

REST and MCP

Call the JSON endpoints from your code or query the scan in Claude, ChatGPT and Cursor at 60 calls a minute.

SEO audit endpoints and MCP tools

The MarketingTracer app runs on these same endpoints. Every reading endpoint takes POST with a JSON body or GET with query parameters. Each one is also a read-only tool in the MCP server. The nine writing endpoints take POST requests and need role user or higher on the site.

REST endpointMCP toolWhat it does
scan.statusget_scan_statusWhether the site was never crawled (not_started), is crawling (crawling) or finished (done), and whether maintenance is active.
scan.summaryget_scan_summaryAudit score now and in the compared week, scanned and blocked page counts, open, new and fixed problems per severity, 12-week score trend, top three rules (start_here) and whole-site checks (https_redirect, www_redirect, hsts, ssl_valid, ssl_expiry, robots_txt, llms_txt).
scan.issuesget_scan_issuesFailing rules ordered by priority: open and checked pages, new, fixed and regressed counts and URLs since the compared week, 28-day Search Console clicks on failing pages, weekly trend and whether the rule is hidden.
scan.foldersget_scan_foldersUp to eight top-level URL folders (/blog, /products, …) with scanned pages, average SEO score, errors, blocked pages and 28-day Search Console clicks.
scan.pagesget_scan_pagesEvery scanned page (paginated with cursor, optional rule filter): HTTP status, indexability and not_indexable reasons, overall and category scores, every rule (true or false), title, meta description, H1, first paragraph, SERP preview, lab speed score and timing_ms, opi, internal PageRank, internal link counts, duplicate_of, ai_content, notes and target keywords.
scan.pageget_scan_pageOne page by page_id or url: everything in scan.pages plus every internal incoming link (from_url, anchor, places in main, nav or footer), 26 weeks of page scores and per-bot AI crawler access (ai_bots).
scan.changesget_scan_changesChanges the scan saw over the last 400 days (site-wide or for one page_id): rules that started failing or passed again, and changed titles, H1s, meta descriptions or HTTP statuses with the value before and after.
scan.historyget_scan_historyWeekly site history over the last 26 weeks: average page and category scores, speed score, failing rules per severity, audit score and the share of checked pages passing each rule.
scan.rulesget_scan_rulesEvery rule with its texts in English (en) or Dutch (nl): title, severity, category, what passing and failing mean, the problem in one line, why it matters, how to fix it, effort (low, medium, high), whether it is hidden, rule categories and duplicate checks.
scan.ai_botsget_scan_ai_botsPer AI search and training crawler whether robots.txt or the server blocks it, up to 500 blocked pages, and the site-wide AI writing signal breakdown (ai_content) with up to 500 scored pages.
scan.clicksget_scan_clicksSearch Console clicks of the last 28 days per normalised address, for joining with scan.pages.
scan.link_statusget_scan_link_statusHTTP status of up to 500 internal addresses as the scan last saw them.
scan.viewsget_scan_viewsYour saved page-list filter views for the site (up to 10 per user).
scan.crawlREST only (POST)Start a crawl of the whole site: the sitemap and every page the crawler finds are scanned again. Follow it with scan.status.
scan.emptyREST only (POST)Delete every scanned page of the site with its links, changes and text vectors. Weekly site history stays intact.
scan.page_addREST only (POST)Add a full URL to the scan and return its page_id.
scan.page_deleteREST only (POST)Remove one page (page_id) with its links and changes from the scan.
scan.page_saveREST only (POST)Save user fields on a page (page_id): starred, notes, keywords and negative_keywords.
scan.rule_hideREST only (POST)Hide a rule (hidden: true) for the site so it does not count in the scores, or count it again (hidden: false).
scan.ai_bots_refreshREST only (POST)Check robots.txt and the server again for every AI crawler and return the updated scan.ai_bots result.
scan.view_saveREST only (POST)Save a named filter view of the page list (up to 10 per user per site).
scan.view_deleteREST only (POST)Delete one of your saved filter views by id.

Composite MCP tools that use the scan

These composite tools in the MCP server join the on-page scan with Search Console and Google Analytics 4 in one call:

MCP toolWhat it returns
get_scanIssue summary across the site with per-page seo_score and optimisation_potential (opi), or one URL's failing rules with help text in English or Dutch.
find_opportunitiesPages worth fixing first, ranked by optimisation_potential and joined with Search Console clicks, impressions and average position.
page_briefSearch Console queries, click trend, Google Analytics 4 traffic and on-page scan issues for one URL.
diagnose_traffic_dropCompares a recent window (default 28 days) against the period before it: which pages lost Search Console clicks, whether Analytics agrees, and what scan.changes logged on those pages while it happened.
cluster_performancePerformance per URL folder (depth 1 to 3): page count, how many pages earn search traffic, Search Console clicks and trend, and scan scores.
get_ai_checkAI readiness from the scan: AI crawler access per bot (ai_crawlers) and the 0-100 stock AI writing score with detected patterns (ai_writing), site-wide or for one URL.

A real scan.summary call and its answer

Ask for the audit summary of a site with your API key in the Authorization header:

curl -X POST https://app.marketingtracer.com/rest/v1/scan.summary \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id": "YOUR_SITE_ID"}'

The response is a real, anonymised answer for a 43-page site, shortened with …:

{
  "data": {
    "crawl": {"state": "done", "maintenance": false, "maintenance_until": null},
    "pages": 43,
    "blocked_pages": 0,
    "score": 82,
    "previous_score": 82,
    "compared_week": "2026-11-02",
    "last_scan_date": "2026-11-10",
    "search_console": true,
    "open": {"errors": 0, "warnings": 56, "notices": 221},
    "new": {"errors": 0, "warnings": 4, "notices": 0},
    "fixed": {"errors": 0, "warnings": 1, "notices": 3},
    "score_trend": [
      {"week": "2026-10-26", "score": 82},
      {"week": "2026-11-02", "score": 82},
      …
    ],
    "start_here": ["readability_score", "links_to_redirect"],
    "site_checks": {
      "checked_at": "2026-11-07T03:22:02Z",
      "checks": [
        {"key": "https_redirect", "severity": "error", "ok": true, "value": "https://www.example-site.nl/"},
        {"key": "www_redirect", "severity": "warning", "ok": false, "value": "https://example-site.nl/"},
        …
      ]
    }
  },
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Run the Monday audit in your own client portal

Your client portal loads scan.summary and scan.issues to show the audit score and what changed since the compared week. Every failing rule includes the 28-day Search Console clicks on the affected pages, so your client sees right away which technical errors touch real search traffic. If a rule does not fit a client's CMS template, an account manager can hide it from your portal with scan.rule_hide so it drops out of the score. When their developer deploys a release, your portal calls scan.crawl and checks scan.status until state is done.

Inspect one URL and hand the fix to your client's team

When a page drops out of Google or loses its score, scan.page gives you the exact reason in not_indexable, such as a canonical pointing elsewhere or a stray noindex header. The same call lists every internal link to that URL split into main, nav and footer, so you can spot a page that is only linked from the footer. Next to it, scan.changes shows 400 days of edits to the title, H1, meta description, HTTP status and rule verdicts, with the value before and after. Call scan.rules in English (en) or Dutch (nl) to put the problem and the fix straight into a ticket for the client's developer.

Ask Claude or ChatGPT why clicks fell and what to fix first

A client calls on Monday because organic clicks dipped last month. In Claude or ChatGPT, diagnose_traffic_drop compares the last 28 days of Search Console and Analytics against the 28 days before. On every page that lost clicks, it attaches the title, H1, status and rule changes the scan logged during that window. When you plan the next sprint, find_opportunities ranks pages by their optimisation potential (opi) alongside Search Console clicks and impressions. For a single URL, page_brief brings together its search queries, Google Analytics traffic, scan score and open issues.

Endpoints, arguments and field descriptions

Every endpoint is a POST with a JSON body; reading endpoints also take a GET with the same arguments in the query string. Send your API key as Authorization: Bearer YOUR_API_KEY. Every answer is {"data": …, "meta": {…}}: meta.site_id always, meta.next_cursor and meta.total for a list, meta.period where the endpoint covers a period. An error is {"error": {"code", "message"}} with the HTTP status: 400 invalid_argument, unknown_argument or missing_argument; 401 not_authenticated; 403 no_access or tool_off; 404 not_found; 409 not_connected (a connection the endpoint needs, such as Google Analytics 4, is missing); 429 rate_limited; 502 google_error (Google returned an error). The machine-readable version is openapi.json.

scan.status

POST /rest/v1/scan.status · MCP tool get_scan_status

Returns whether the site is being crawled, was crawled, or was never crawled, and whether the scan is paused for maintenance. Poll it while a crawl runs.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.status \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": {"state": "done", "maintenance": false, "maintenance_until": null},
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
statestringnot_started (the site was never crawled), crawling (a crawl is running) or done.
maintenancebooleanWhether the scan is paused for maintenance (crawls and live scans wait).
maintenance_untilstring or nullUntil when, ISO 8601 UTC, when known.

scan.summary

POST /rest/v1/scan.summary · MCP tool get_scan_summary

Returns the state of the SEO audit of the site: pages, the audit score now and the week before, open problems per severity, what became a problem and what was fixed since the week we compare with, the score per week, the three rules that gain most ("start here") and the site-wide checks (https, www, HSTS, SSL, robots.txt, llms.txt). Pages the server refused (blocked) do not count. new and fixed are 0 until there are two weeks to compare.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.summary \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": {
    "crawl": {"state": "done", "maintenance": false, "maintenance_until": null},
    "pages": 43,
    "blocked_pages": 0,
    "score": 82,
    "previous_score": 82,
    "compared_week": "2026-11-02",
    "last_scan_date": "2026-11-10",
    "search_console": true,
    "open": {"errors": 0, "warnings": 56, "notices": 221},
    "new": {"errors": 0, "warnings": 4, "notices": 0},
    "fixed": {"errors": 0, "warnings": 1, "notices": 3},
    "score_trend": [{"week": "2026-10-26", "score": 82}, {"week": "2026-11-02", "score": 82}],
    "trend_weeks": ["2025-10-27", "2025-11-03"],
    "start_here": ["readability_score", "links_to_redirect"],
    "site_checks": {
      "checked_at": "2026-11-07T03:22:02Z",
      "checks": [
        {
          "key": "https_redirect",
          "severity": "error",
          "ok": true,
          "value": "https://www.example-site.nl/"
        },
        {
          "key": "www_redirect",
          "severity": "warning",
          "ok": false,
          "value": "https://example-site.nl/"
        }
      ]
    }
  },
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
crawlobjectThe crawl, as in scan.status.
crawl.statestringnot_started (the site was never crawled), crawling (a crawl is running) or done.
crawl.maintenancebooleanWhether the scan is paused for maintenance (crawls and live scans wait).
crawl.maintenance_untilstring or nullUntil when, ISO 8601 UTC, when known.
pagesintegerScanned pages.
blocked_pagesintegerPages the server refused (firewall); left out of every number.
scoreinteger or nullThe audit score, 0-100: the share of passed checks over all visible rules with a severity; null without scanned pages.
previous_scoreinteger or nullThe same score in the week we compare with; null when not known.
compared_weekstring or nullThe week we compare with (its Monday, YYYY-MM-DD); null with less than two weeks of history.
last_scan_datestring or nullThe day the last page was scanned, YYYY-MM-DD.
search_consolebooleanWhether Search Console clicks are used (connected, and Search Insights is on for you).
openobjectFailing page × rule combinations per severity.
open.errorsintegerSeverity error.
open.warningsintegerSeverity warning.
open.noticesintegerSeverity notice.
newobjectNew and back-again problems since the compared week per severity.
new.errorsintegerSeverity error.
new.warningsintegerSeverity warning.
new.noticesintegerSeverity notice.
fixedobjectProblems fixed since the compared week per severity.
fixed.errorsintegerSeverity error.
fixed.warningsintegerSeverity warning.
fixed.noticesintegerSeverity notice.
score_trendarrayThe audit score per week, the last 12 weeks.
score_trend[].weekstringThe Monday, YYYY-MM-DD.
score_trend[].scoreinteger or nullThe audit score that week.
trend_weeksarray<string>The weeks of the trend in scan.issues (Mondays, YYYY-MM-DD); the trend has one more value: now.
start_herearray<string>The rule keys that gain most when fixed, up to three.
site_checksobject or nullChecks of the site itself, null when never checked.
site_checks.checked_atstring or nullWhen, ISO 8601 UTC.
site_checks.checksarrayOne per check.
site_checks.checks[].keystringhttps_redirect, www_redirect, hsts, ssl_valid, ssl_expiry, robots_txt, llms_txt.
site_checks.checks[].severitystringerror, warning, notice or info.
site_checks.checks[].okbooleanWhether it passes.
site_checks.checks[].valuestring or nullWhat we found: where http redirects to, the certificate issuer, days until it expires, …

scan.issues

POST /rest/v1/scan.issues · MCP tool get_scan_issues

Returns the problems of the site, one row per rule with a severity, the ones that gain most when fixed first (hidden rules last): how many pages fail and are checked, what is new, fixed and back again since the compared week (null without one), the clicks on the failing pages (null without Search Console), the number of failing pages per week, and the pages that are new or back again. Blocked pages and redirects do not count.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.issues \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": [
    {
      "rule": "readability_score",
      "category": "technical",
      "severity": "warning",
      "open_pages": 35,
      "checked_pages": 37,
      "new": 0,
      "fixed": 1,
      "regressed": 0,
      "clicks": 460,
      "trend": [null, null],
      "impact": 243,
      "hidden": false,
      "new_pages": [],
      "regressed_pages": []
    },
    {
      "rule": "links_to_redirect",
      "category": "technical",
      "severity": "warning",
      "open_pages": 5,
      "checked_pages": 37,
      "new": 0,
      "fixed": 0,
      "regressed": 0,
      "clicks": 363,
      "trend": [null, null],
      "impact": 123.89999999999999,
      "hidden": false,
      "new_pages": [],
      "regressed_pages": []
    }
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 49}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
rulestringThe rule key (see scan.rules).
categorystringindexability, content, mobile, technical, serps, social, …
severitystringerror, warning or notice.
open_pagesintegerPages where the rule fails.
checked_pagesintegerPages where the rule was checked.
newinteger or nullPages where it started failing since the compared week; null without one.
fixedinteger or nullPages where it was fixed since then.
regressedinteger or nullPages where it fails again after having been fixed before.
clicksinteger or nullSearch Console clicks (28 days) on the failing pages; null without Search Console.
trendarray or nullFailing pages per week of scan.summary trend_weeks, then now; a value is null for a week without counts; null when no week has counts.
impactnumberHow much fixing the rule gains, for sorting (higher first); 0 for a hidden rule.
hiddenbooleanWhether the rule is hidden for this site (scan.rule_hide); it then does not count in the score.
new_pagesarray<string>Pages where the rule became a problem since the compared week.
regressed_pagesarray<string>Pages where it is a problem again.

scan.folders

POST /rest/v1/scan.folders · MCP tool get_scan_folders

Returns the site per first folder (/blog, /products, …), most pages first, up to 8: pages, the average page score, errors, blocked pages and Search Console clicks (null without Search Console).

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.folders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": [
    {
      "path": "/blog",
      "pages": 11,
      "score": 78,
      "errors": 12,
      "blocked_pages": 0,
      "clicks": 18
    },
    {
      "path": "/doula",
      "pages": 9,
      "score": 81,
      "errors": 7,
      "blocked_pages": 0,
      "clicks": 2
    }
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 8}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
pathstringThe folder, e.g. /blog; / for pages in the root.
pagesintegerScanned pages in it.
scoreinteger or nullAverage SEO score of its HTML pages; null when there are none.
errorsintegerFailing rules of severity error, summed over its pages.
blocked_pagesintegerPages the server refused.
clicksinteger or nullSearch Console clicks in 28 days; null without Search Console.

scan.pages

POST /rest/v1/scan.pages · MCP tool get_scan_pages

Returns every scanned page of the site with its status, scores, the result of every rule, its title, description and H1, speed and internal links. Page through with cursor. With rule only the pages where that rule fails.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
rulestringOnly pages where this rule fails (a key from scan.rules).
limitinteger, 1 to 5,000, default 1,000
cursorstringmeta.next_cursor of the previous answer.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.pages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","limit":2}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": [
    {
      "id": "6ae33f01ec26871756010ebc",
      "url": "https://www.example-site.nl/",
      "depth": 0,
      "in_sitemap": false,
      "scanned_at": "2026-11-08T04:19:06Z",
      "http_status": 200,
      "type": "page",
      "content_type": "text",
      "redirect_url": null,
      "language": "nl",
      "country": null,
      "indexable": true,
      "not_indexable": [],
      "blocked": false,
      "score": 85,
      "scores": {
        "indexability": 91,
        "content": 86,
        "mobile": 100,
        "technical": 76,
        "serps": 83,
        "social": 100
      },
      "errors": 0,
      "warnings": 2,
      "notices": 5,
      "rules": {
        "indexable_by_meta_tag": true,
        "indexable_by_canonical": true,
        "indexable_by_robots_txt": true,
        "indexable_by_header": true,
        "indexable": true,
        "in_sitemap": false,
        "ai_search_bots_allowed": true,
        "h1_found": true,
        "normal_headings": false,
        "h1_set_once": true,
        "lang_set": true,
        "ssl": true,
        "redirect_chain_length": true,
        "title_exists": true,
        "title_length": false,
        "metadescription_exists": true,
        "metadescription_length": true,
        "og_complete": true,
        "readability_score": false,
        "wordcount_over_200": true,
        "main_element_exists": false,
        "canonical_exists": true,
        "clean_url": true,
        "fit_in_viewport": true,
        "viewport_detected": true,
        "viewport_ok": true,
        "viewport_zoom": true,
        "title_relevant": true,
        "h1_relevant": true,
        "metadescription_relevant": true,
        "pfirst_relevant": true,
        "compression": true,
        "describe_images": true,
        "set_image_dimensions": true,
        "correct_image_dimensions": false,
        "lazy_load_images": true,
        "all_internal_links_are_dofollow": true,
        "correct_link_subdomain": true,
        "inlinks_relevant": true,
        "valid_local_response_interceptions": true,
        "valid_response_interceptions": false,
        "third_party_files_load": true,
        "no_mixed_content": true,
        "lcp_not_lazy": true,
        "content_without_js": true,
        "image_file_size": true,
        "links_to_broken": true,
        "links_to_redirect": false,
        "depth_ok": true,
        "unique_title": true,
        "unique_metadescription": true,
        "unique_h1": true,
        "unique_content": true
      },
      "title": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage Utrecht - Voorbeeld Studio",
      "meta_description": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage in Utrecht. Vol kennis, vol liefde en vol vertrouwen zwanger zijn en ouders worden.",
      "h1": "Welkom bij Voorbeeld Studio!",
      "first_paragraph": "Je bent zwanger, gefeliciteerd! Je hebt de controles geregeld bij de verloskundige of gynaecoloog, misschien heb je de kraamzorg al geregeld en je bent nu op...",
      "serp_title": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage Utrecht - Voorbeeld Studio",
      "serp_description": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage in Utrecht. Vol kennis, vol liefde en vol vertrouwen zwanger zijn en ouders worden.",
      "serp_url": "https://www.example-site.nl/",
      "speed_score": 99,
      "timing_ms": {
        "first_paint": 252,
        "first_contentful_paint": 252,
        "largest_contentful_paint": 364,
        "ttfb": 77,
        "dom_content_loaded": 151,
        "onload": 1326,
        "dns": 20,
        "dom_interactive": 106,
        "load_event_end": 1326,
        "cls": 0
      },
      "opi": 64.52,
      "pagerank": 100,
      "starred": false,
      "inlinks": {"total": 43, "main": 0, "nav": 43, "footer": 0},
      "duplicate_of": [],
      "ai_content": {
        "version": 3,
        "language": "nl",
        "words": 356,
        "score": 7,
        "level": "low",
        "signals": [{"key": "triplets", "count": 2, "points": 7.2}],
        "paragraphs": {"high": 0, "medium": 0, "low": 4},
        "reading_score": 57,
        "checked_at": "2026-11-08T04:19:06Z"
      },
      "notes": [],
      "keywords": "",
      "negative_keywords": ""
    },
    {
      "id": "6ae33f01ec26871756010ebc",
      "url": "https://www.example-site.nl/",
      "depth": 0,
      "in_sitemap": false,
      "scanned_at": "2026-11-08T04:19:06Z",
      "http_status": 200,
      "type": "page",
      "content_type": "text",
      "redirect_url": null,
      "language": "nl",
      "country": null,
      "indexable": true,
      "not_indexable": [],
      "blocked": false,
      "score": 85,
      "scores": {
        "indexability": 91,
        "content": 86,
        "mobile": 100,
        "technical": 76,
        "serps": 83,
        "social": 100
      },
      "errors": 0,
      "warnings": 2,
      "notices": 5,
      "rules": {
        "indexable_by_meta_tag": true,
        "indexable_by_canonical": true,
        "indexable_by_robots_txt": true,
        "indexable_by_header": true,
        "indexable": true,
        "in_sitemap": false,
        "ai_search_bots_allowed": true,
        "h1_found": true,
        "normal_headings": false,
        "h1_set_once": true,
        "lang_set": true,
        "ssl": true,
        "redirect_chain_length": true,
        "title_exists": true,
        "title_length": false,
        "metadescription_exists": true,
        "metadescription_length": true,
        "og_complete": true,
        "readability_score": false,
        "wordcount_over_200": true,
        "main_element_exists": false,
        "canonical_exists": true,
        "clean_url": true,
        "fit_in_viewport": true,
        "viewport_detected": true,
        "viewport_ok": true,
        "viewport_zoom": true,
        "title_relevant": true,
        "h1_relevant": true,
        "metadescription_relevant": true,
        "pfirst_relevant": true,
        "compression": true,
        "describe_images": true,
        "set_image_dimensions": true,
        "correct_image_dimensions": false,
        "lazy_load_images": true,
        "all_internal_links_are_dofollow": true,
        "correct_link_subdomain": true,
        "inlinks_relevant": true,
        "valid_local_response_interceptions": true,
        "valid_response_interceptions": false,
        "third_party_files_load": true,
        "no_mixed_content": true,
        "lcp_not_lazy": true,
        "content_without_js": true,
        "image_file_size": true,
        "links_to_broken": true,
        "links_to_redirect": false,
        "depth_ok": true,
        "unique_title": true,
        "unique_metadescription": true,
        "unique_h1": true,
        "unique_content": true
      },
      "title": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage Utrecht - Voorbeeld Studio",
      "meta_description": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage in Utrecht. Vol kennis, vol liefde en vol vertrouwen zwanger zijn en ouders worden.",
      "h1": "Welkom bij Voorbeeld Studio!",
      "first_paragraph": "Je bent zwanger, gefeliciteerd! Je hebt de controles geregeld bij de verloskundige of gynaecoloog, misschien heb je de kraamzorg al geregeld en je bent nu op...",
      "serp_title": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage Utrecht - Voorbeeld Studio",
      "serp_description": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage in Utrecht. Vol kennis, vol liefde en vol vertrouwen zwanger zijn en ouders worden.",
      "serp_url": "https://www.example-site.nl/",
      "speed_score": 99,
      "timing_ms": {
        "first_paint": 252,
        "first_contentful_paint": 252,
        "largest_contentful_paint": 364,
        "ttfb": 77,
        "dom_content_loaded": 151,
        "onload": 1326,
        "dns": 20,
        "dom_interactive": 106,
        "load_event_end": 1326,
        "cls": 0
      },
      "opi": 64.52,
      "pagerank": 100,
      "starred": false,
      "inlinks": {"total": 43, "main": 0, "nav": 43, "footer": 0},
      "duplicate_of": [],
      "ai_content": {
        "version": 3,
        "language": "nl",
        "words": 356,
        "score": 7,
        "level": "low",
        "signals": [{"key": "triplets", "count": 2, "points": 7.2}],
        "paragraphs": {"high": 0, "medium": 0, "low": 4},
        "reading_score": 57,
        "checked_at": "2026-11-08T04:19:06Z"
      },
      "notes": [],
      "keywords": "",
      "negative_keywords": ""
    }
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": "6ae33f016edabf178406fd72", "total": 43}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
idstringThe page id (page_id elsewhere).
urlstringThe address of the page.
depthinteger or nullClicks from the homepage (0 = the homepage); null when not known.
in_sitemapbooleanWhether the address is in the XML sitemap.
scanned_atstring or nullWhen the page was last scanned, ISO 8601 UTC; null when it was found but not scanned yet.
http_statusinteger or nullThe HTTP status the page answered with; 0 when it did not answer; null before the first scan.
typestring or nullpage (an HTML page), redirect, or the kind of file; null before the first scan.
content_typestring or nullThe content type: text, image, pdf, …
redirect_urlstring or nullWhere a redirect points to; null when it is no redirect.
languagestring or nullThe language the page says it is in (html lang).
countrystring or nullThe country the page says it is for, when it says so.
indexableboolean or nullWhether Google may index the page; null before the first scan.
not_indexablearray or nullWhy the page is not indexable; empty when it is; null for pages scanned before 29 October 2026.
not_indexable[].kindstringstatus (the HTTP status), type (not HTML), meta (robots meta tag), canonical (points elsewhere), robots (robots.txt), header (X-Robots-Tag).
not_indexable[].valuestring or nullWhat we found: the status code, the meta value, where the canonical points, the robots.txt rule.
blockedbooleanWhether the server refused our scanner (401, 403, 429, 5xx or no answer, usually a firewall). A blocked page is left out of every count and score.
scorenumber or nullThe SEO score of the page, 0-100; null before the first scan.
scoresobjectThe score per category, 0-100 or null.
scores.indexabilitynumber or nullCan Google find and index the page.
scores.contentnumber or nullHeadings, text, images.
scores.mobilenumber or nullHow the page works on a phone.
scores.technicalnumber or nullSpeed, links, files, security.
scores.serpsnumber or nullHow the page shows in Google: title, description.
scores.socialnumber or nullSharing on social media.
errorsintegerFailing rules of severity error on this page.
warningsintegerFailing rules of severity warning.
noticesintegerFailing rules of severity notice.
rulesobjectEvery rule checked on this page: rule key (see scan.rules) → true (passes) or false (fails). Site-wide checks (duplicate titles, orphan pages, …) are included.
titlestringThe title tag.
meta_descriptionstringThe meta description.
h1stringThe first H1.
first_paragraphstringThe first paragraph of the main text.
serp_titlestringThe title as Google will probably show it (cut to fit).
serp_descriptionstringThe description as Google will probably show it.
serp_urlstring or nullThe address as it appears in Google (the canonical address when the page has one).
speed_scorenumber or nullThe speed score of the scan, 0-100.
timing_msobject or nullThe browser timings of the scan in milliseconds (first_paint, first_contentful_paint, largest_contentful_paint, ttfb, dom_content_loaded, dom_interactive, onload, load_event_end, dns) and cls (layout shift, no unit); null without a scan in Chrome.
opinumberOptimisation potential: how much the page would gain from fixing its problems, weighed by its importance; higher = fix first.
pageranknumberInternal PageRank, 0-100: how strong the page is in the internal link structure.
starredbooleanWhether someone starred the page.
inlinksobjectInternal links to this page, per place on the linking page.
inlinks.totalintegerAll internal links to the page.
inlinks.mainintegerIn the main text.
inlinks.navintegerIn the menu.
inlinks.footerintegerIn the footer.
duplicate_ofarray<string>Pages with (almost) the same text (rule unique_content), up to 5 addresses; empty when there are none.
ai_contentobject or nullAI signals: how much the main text reads like stock AI writing (Dutch and English only); null when not checked.
ai_content.versioninteger or nullVersion of the scoring.
ai_content.languagestring or nullLanguage of the text: nl or en.
ai_content.wordsinteger or nullWords in the main text.
ai_content.scorenumber or null0-100: how much the text reads like stock AI writing.
ai_content.levelstring or nulllow, medium, high, too_short or unsupported.
ai_content.signalsarrayThe signals that counted most.
ai_content.signals[].keystringThe signal, e.g. phrases, em_dash, triplets.
ai_content.signals[].countnumber or nullHow often it occurs (weighted: some signals count half).
ai_content.signals[].pointsnumber or nullWhat it adds to the score.
ai_content.paragraphsobject or nullParagraphs per level.
ai_content.paragraphs.highintegerParagraphs that read like AI.
ai_content.paragraphs.mediumintegerParagraphs in between.
ai_content.paragraphs.lowintegerParagraphs that read like a person.
ai_content.reading_scorenumber or nullReading ease (Flesch-Douma for nl, Flesch for en).
ai_content.checked_atstring or nullWhen it was checked, ISO 8601 UTC.
notesarrayNotes people added to the page.
keywordsstringThe keywords the page should be relevant for (set by the user; used by the relevance rules).
negative_keywordsstringKeywords the page should not be about.

scan.page

POST /rest/v1/scan.page · MCP tool get_scan_page

Returns one page as stored by the last scan (by page_id or url): everything in scan.pages, plus every internal link to it, its score per week and which AI crawlers may read it. A fresh scan: scan.page_rescan.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
page_idstringThe page id from scan.pages.
urlstringOr the address of the page.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.page \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","url":"https://www.example-site.nl/"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": {
    "id": "6ae33f01ec26871756010ebc",
    "url": "https://www.example-site.nl/",
    "depth": 0,
    "in_sitemap": false,
    "scanned_at": "2026-11-08T04:19:06Z",
    "http_status": 200,
    "type": "page",
    "content_type": "text",
    "redirect_url": null,
    "language": "nl",
    "country": null,
    "indexable": true,
    "not_indexable": [],
    "blocked": false,
    "score": 85,
    "scores": {
      "indexability": 91,
      "content": 86,
      "mobile": 100,
      "technical": 76,
      "serps": 83,
      "social": 100
    },
    "errors": 0,
    "warnings": 2,
    "notices": 5,
    "rules": {
      "indexable_by_meta_tag": true,
      "indexable_by_canonical": true,
      "indexable_by_robots_txt": true,
      "indexable_by_header": true,
      "indexable": true,
      "in_sitemap": false,
      "ai_search_bots_allowed": true,
      "h1_found": true,
      "normal_headings": false,
      "h1_set_once": true,
      "lang_set": true,
      "ssl": true,
      "redirect_chain_length": true,
      "title_exists": true,
      "title_length": false,
      "metadescription_exists": true,
      "metadescription_length": true,
      "og_complete": true,
      "readability_score": false,
      "wordcount_over_200": true,
      "main_element_exists": false,
      "canonical_exists": true,
      "clean_url": true,
      "fit_in_viewport": true,
      "viewport_detected": true,
      "viewport_ok": true,
      "viewport_zoom": true,
      "title_relevant": true,
      "h1_relevant": true,
      "metadescription_relevant": true,
      "pfirst_relevant": true,
      "compression": true,
      "describe_images": true,
      "set_image_dimensions": true,
      "correct_image_dimensions": false,
      "lazy_load_images": true,
      "all_internal_links_are_dofollow": true,
      "correct_link_subdomain": true,
      "inlinks_relevant": true,
      "valid_local_response_interceptions": true,
      "valid_response_interceptions": false,
      "third_party_files_load": true,
      "no_mixed_content": true,
      "lcp_not_lazy": true,
      "content_without_js": true,
      "image_file_size": true,
      "links_to_broken": true,
      "links_to_redirect": false,
      "depth_ok": true,
      "unique_title": true,
      "unique_metadescription": true,
      "unique_h1": true,
      "unique_content": true
    },
    "title": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage Utrecht - Voorbeeld Studio",
    "meta_description": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage in Utrecht. Vol kennis, vol liefde en vol vertrouwen zwanger zijn en ouders worden.",
    "h1": "Welkom bij Voorbeeld Studio!",
    "first_paragraph": "Je bent zwanger, gefeliciteerd! Je hebt de controles geregeld bij de verloskundige of gynaecoloog, misschien heb je de kraamzorg al geregeld en je bent nu op...",
    "serp_title": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage Utrecht - Voorbeeld Studio",
    "serp_description": "Zwangerschapsyoga, Hypnobirthing® en zwangerschapsmassage in Utrecht. Vol kennis, vol liefde en vol vertrouwen zwanger zijn en ouders worden.",
    "serp_url": "https://www.example-site.nl/",
    "speed_score": 99,
    "timing_ms": {
      "first_paint": 252,
      "first_contentful_paint": 252,
      "largest_contentful_paint": 364,
      "ttfb": 77,
      "dom_content_loaded": 151,
      "onload": 1326,
      "dns": 20,
      "dom_interactive": 106,
      "load_event_end": 1326,
      "cls": 0
    },
    "opi": 64.52,
    "pagerank": 100,
    "starred": false,
    "inlinks": {"total": 43, "main": 0, "nav": 43, "footer": 0},
    "duplicate_of": [],
    "ai_content": {
      "version": 3,
      "language": "nl",
      "words": 356,
      "score": 7,
      "level": "low",
      "signals": [{"key": "triplets", "count": 2, "points": 7.2}],
      "paragraphs": {"high": 0, "medium": 0, "low": 4},
      "reading_score": 57,
      "checked_at": "2026-11-08T04:19:06Z"
    },
    "notes": [],
    "keywords": "",
    "negative_keywords": "",
    "incoming_links": [
      {
        "from_url": "https://www.example-site.nl/",
        "anchor": "Voorbeeld Studio",
        "places": [{"text": "Voorbeeld Studio", "place": "nav"}]
      },
      {
        "from_url": "https://www.example-site.nl/zwangerschapsyoga",
        "anchor": "Voorbeeld Studio",
        "places": [{"text": "Voorbeeld Studio", "place": "nav"}]
      }
    ],
    "weeks": [
      {
        "week": "2026-10-26",
        "score": 82,
        "scores": {
          "indexability": 90,
          "content": 90,
          "mobile": 86,
          "technical": 73,
          "serps": 83,
          "social": 100
        }
      },
      {
        "week": "2026-11-02",
        "score": 85,
        "scores": {
          "indexability": 91,
          "content": 86,
          "mobile": 100,
          "technical": 76,
          "serps": 83,
          "social": 100
        }
      }
    ],
    "ai_bots": [
      {
        "bot": "OAI-SearchBot",
        "owner": "OpenAI (ChatGPT search)",
        "type": "search",
        "blocked": false,
        "blocked_by": null
      },
      {
        "bot": "ChatGPT-User",
        "owner": "OpenAI (ChatGPT)",
        "type": "search",
        "blocked": false,
        "blocked_by": null
      }
    ]
  },
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
idstringThe page id (page_id elsewhere).
urlstringThe address of the page.
depthinteger or nullClicks from the homepage (0 = the homepage); null when not known.
in_sitemapbooleanWhether the address is in the XML sitemap.
scanned_atstring or nullWhen the page was last scanned, ISO 8601 UTC; null when it was found but not scanned yet.
http_statusinteger or nullThe HTTP status the page answered with; 0 when it did not answer; null before the first scan.
typestring or nullpage (an HTML page), redirect, or the kind of file; null before the first scan.
content_typestring or nullThe content type: text, image, pdf, …
redirect_urlstring or nullWhere a redirect points to; null when it is no redirect.
languagestring or nullThe language the page says it is in (html lang).
countrystring or nullThe country the page says it is for, when it says so.
indexableboolean or nullWhether Google may index the page; null before the first scan.
not_indexablearray or nullWhy the page is not indexable; empty when it is; null for pages scanned before 29 October 2026.
not_indexable[].kindstringstatus (the HTTP status), type (not HTML), meta (robots meta tag), canonical (points elsewhere), robots (robots.txt), header (X-Robots-Tag).
not_indexable[].valuestring or nullWhat we found: the status code, the meta value, where the canonical points, the robots.txt rule.
blockedbooleanWhether the server refused our scanner (401, 403, 429, 5xx or no answer, usually a firewall). A blocked page is left out of every count and score.
scorenumber or nullThe SEO score of the page, 0-100; null before the first scan.
scoresobjectThe score per category, 0-100 or null.
scores.indexabilitynumber or nullCan Google find and index the page.
scores.contentnumber or nullHeadings, text, images.
scores.mobilenumber or nullHow the page works on a phone.
scores.technicalnumber or nullSpeed, links, files, security.
scores.serpsnumber or nullHow the page shows in Google: title, description.
scores.socialnumber or nullSharing on social media.
errorsintegerFailing rules of severity error on this page.
warningsintegerFailing rules of severity warning.
noticesintegerFailing rules of severity notice.
rulesobjectEvery rule checked on this page: rule key (see scan.rules) → true (passes) or false (fails). Site-wide checks (duplicate titles, orphan pages, …) are included.
titlestringThe title tag.
meta_descriptionstringThe meta description.
h1stringThe first H1.
first_paragraphstringThe first paragraph of the main text.
serp_titlestringThe title as Google will probably show it (cut to fit).
serp_descriptionstringThe description as Google will probably show it.
serp_urlstring or nullThe address as it appears in Google (the canonical address when the page has one).
speed_scorenumber or nullThe speed score of the scan, 0-100.
timing_msobject or nullThe browser timings of the scan in milliseconds (first_paint, first_contentful_paint, largest_contentful_paint, ttfb, dom_content_loaded, dom_interactive, onload, load_event_end, dns) and cls (layout shift, no unit); null without a scan in Chrome.
opinumberOptimisation potential: how much the page would gain from fixing its problems, weighed by its importance; higher = fix first.
pageranknumberInternal PageRank, 0-100: how strong the page is in the internal link structure.
starredbooleanWhether someone starred the page.
inlinksobjectInternal links to this page, per place on the linking page.
inlinks.totalintegerAll internal links to the page.
inlinks.mainintegerIn the main text.
inlinks.navintegerIn the menu.
inlinks.footerintegerIn the footer.
duplicate_ofarray<string>Pages with (almost) the same text (rule unique_content), up to 5 addresses; empty when there are none.
ai_contentobject or nullAI signals: how much the main text reads like stock AI writing (Dutch and English only); null when not checked.
ai_content.versioninteger or nullVersion of the scoring.
ai_content.languagestring or nullLanguage of the text: nl or en.
ai_content.wordsinteger or nullWords in the main text.
ai_content.scorenumber or null0-100: how much the text reads like stock AI writing.
ai_content.levelstring or nulllow, medium, high, too_short or unsupported.
ai_content.signalsarrayThe signals that counted most.
ai_content.signals[].keystringThe signal, e.g. phrases, em_dash, triplets.
ai_content.signals[].countnumber or nullHow often it occurs (weighted: some signals count half).
ai_content.signals[].pointsnumber or nullWhat it adds to the score.
ai_content.paragraphsobject or nullParagraphs per level.
ai_content.paragraphs.highintegerParagraphs that read like AI.
ai_content.paragraphs.mediumintegerParagraphs in between.
ai_content.paragraphs.lowintegerParagraphs that read like a person.
ai_content.reading_scorenumber or nullReading ease (Flesch-Douma for nl, Flesch for en).
ai_content.checked_atstring or nullWhen it was checked, ISO 8601 UTC.
notesarrayNotes people added to the page.
keywordsstringThe keywords the page should be relevant for (set by the user; used by the relevance rules).
negative_keywordsstringKeywords the page should not be about.
incoming_linksarrayEvery internal link to this page.
incoming_links[].from_urlstringThe page that links here.
incoming_links[].anchorstringThe link text.
incoming_links[].placesarrayWhere on the linking page the link is (a page can link more than once).
incoming_links[].places[].textstringThe link text.
incoming_links[].places[].placestringmain (in the text), nav (menu) or footer.
weeksarrayThe score of this page per week (last 26 weeks), oldest first.
weeks[].weekstringThe Monday of the week, YYYY-MM-DD.
weeks[].scorenumber or nullThe SEO score that week.
weeks[].scoresobjectThe score per category that week (indexability, content, mobile, technical, serps, social).
ai_botsarray or nullWhich AI crawlers may read this page; null when not checked.
ai_bots[].botstringThe crawler, e.g. GPTBot.
ai_bots[].ownerstringWho runs it.
ai_bots[].typestringsearch (fetches pages to answer a question) or training (collects text to train a model).
ai_bots[].blockedbooleanWhether this crawler is blocked from the page.
ai_bots[].blocked_bystring or nullWhat blocks it: robots.txt, meta or server; null when it is not blocked.

scan.changes

POST /rest/v1/scan.changes · MCP tool get_scan_changes

Returns what the scan saw change on the site, newest first: rules that started failing or were fixed, and changed titles, H1s, descriptions and statuses (with the value before and after). Kept 400 days.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
page_idstringOnly this page.
limitinteger, 1 to 500, default 200

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.changes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","limit":2}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": [
    {
      "date": "2026-11-08",
      "url": "https://www.example-site.nl/zwangerschapsyoga/waarom-zwangerschapsyoga",
      "page_id": "6ae33f046edabf178406fd85",
      "rule": "metadescription_relevant",
      "kind": "fixed",
      "from": null,
      "to": null
    },
    {
      "date": "2026-11-08",
      "url": "https://www.example-site.nl/zwangerschapsyoga/waarom-zwangerschapsyoga",
      "page_id": "6ae33f046edabf178406fd85",
      "rule": "valid_response_interceptions",
      "kind": "new",
      "from": null,
      "to": null
    }
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 9}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
datestringThe day the scan saw the change, YYYY-MM-DD.
urlstringThe page.
page_idstring or nullThe page id.
rulestringThe rule (see scan.rules), or title_change, h1_change, metadescription_change, status_change.
kindstringnew (the rule started failing), fixed (it passes again) or changed (a value changed: title, H1, description, status).
fromstring or nullThe value before, for changed; otherwise null.
tostring or nullThe value after, for changed; otherwise null.

scan.history

POST /rest/v1/scan.history · MCP tool get_scan_history

Returns the site per week (the last 26), oldest first: the average SEO score and per category, the speed score, failing rules per severity, the audit score and per rule the share of pages that pass. null where a week has no value.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.history \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items. Per rule only a few shown.

{
  "data": [
    {
      "week": "2026-11-02",
      "score": 83.62017804154303,
      "scores": {
        "indexability": 90.75425790754258,
        "content": 88.47457627118645,
        "mobile": 98.24561403508771,
        "technical": 73.60594795539033,
        "serps": 80,
        "social": 94.5945945945946
      },
      "speed_score": 99,
      "errors": 36,
      "warnings": 100,
      "notices": 213,
      "audit_score": 82,
      "rules": {
        "indexable_by_meta_tag": 100,
        "indexable_by_canonical": 100,
        "indexable_by_robots_txt": 100,
        "indexable_by_header": 100,
        "indexable": 100,
        "in_sitemap": 0,
        "ai_search_bots_allowed": 100,
        "h1_found": 97.2972972972973,
        "normal_headings": 89.1891891891892,
        "h1_set_once": 89.1891891891892,
        "lang_set": 100,
        "ssl": 100,
        "redirect_chain_length": 100,
        "title_exists": 100,
        "title_length": 35.13513513513514,
        "metadescription_exists": 94.5945945945946,
        "metadescription_length": 62.857142857142854,
        "og_complete": 94.5945945945946,
        "readability_score": 5.405405405405405,
        "wordcount_over_200": 94.5945945945946,
        "main_element_exists": 0,
        "canonical_exists": 100,
        "clean_url": 97.2972972972973,
        "fit_in_viewport": 94.5945945945946,
        "viewport_detected": 100,
        "viewport_ok": 100,
        "viewport_zoom": 100,
        "title_relevant": 94.5945945945946,
        "h1_relevant": 83.78378378378379,
        "metadescription_relevant": 80,
        "pfirst_relevant": 91.8918918918919,
        "compression": 100,
        "describe_images": 81.08108108108108,
        "set_image_dimensions": 54.054054054054056,
        "correct_image_dimensions": 0,
        "lazy_load_images": 100,
        "all_internal_links_are_dofollow": 100,
        "correct_link_subdomain": 100,
        "inlinks_relevant": 62.16216216216216,
        "valid_local_response_interceptions": 100,
        "valid_response_interceptions": 91.8918918918919,
        "third_party_files_load": 100,
        "no_mixed_content": 100,
        "lcp_not_lazy": 100,
        "content_without_js": 100,
        "image_file_size": 75,
        "links_to_broken": 100,
        "links_to_redirect": 86.48648648648648,
        "depth_ok": 100,
        "unique_title": 86.48648648648648,
        "unique_metadescription": 85.71428571428571,
        "unique_h1": 100
      }
    },
    {
      "week": "2026-11-09",
      "score": 83.66745283018868,
      "scores": {
        "indexability": 90.77669902912622,
        "content": 88.62876254180601,
        "mobile": 98.26086956521739,
        "technical": 73.48066298342542,
        "serps": 80.3448275862069,
        "social": 94.5945945945946
      },
      "speed_score": 99,
      "errors": 35,
      "warnings": 101,
      "notices": 212,
      "audit_score": 82,
      "rules": {
        "indexable_by_meta_tag": 100,
        "indexable_by_canonical": 100,
        "indexable_by_robots_txt": 100,
        "indexable_by_header": 100,
        "indexable": 100,
        "in_sitemap": 0,
        "ai_search_bots_allowed": 100,
        "h1_found": 97.2972972972973,
        "normal_headings": 89.1891891891892,
        "h1_set_once": 89.1891891891892,
        "lang_set": 100,
        "ssl": 100,
        "redirect_chain_length": 100,
        "title_exists": 100,
        "title_length": 35.13513513513514,
        "metadescription_exists": 94.5945945945946,
        "metadescription_length": 62.857142857142854,
        "og_complete": 94.5945945945946,
        "readability_score": 5.405405405405405,
        "wordcount_over_200": 94.5945945945946,
        "main_element_exists": 0,
        "canonical_exists": 100,
        "clean_url": 97.2972972972973,
        "fit_in_viewport": 94.5945945945946,
        "viewport_detected": 100,
        "viewport_ok": 100,
        "viewport_zoom": 100,
        "title_relevant": 94.5945945945946,
        "h1_relevant": 83.78378378378379,
        "metadescription_relevant": 82.85714285714286,
        "pfirst_relevant": 91.8918918918919,
        "compression": 100,
        "describe_images": 81.08108108108108,
        "set_image_dimensions": 54.054054054054056,
        "correct_image_dimensions": 0,
        "lazy_load_images": 100,
        "all_internal_links_are_dofollow": 100,
        "correct_link_subdomain": 100,
        "inlinks_relevant": 62.16216216216216,
        "valid_local_response_interceptions": 100,
        "valid_response_interceptions": 86.48648648648648,
        "third_party_files_load": 100,
        "no_mixed_content": 100,
        "lcp_not_lazy": 100,
        "content_without_js": 100,
        "image_file_size": 80,
        "links_to_broken": 100,
        "links_to_redirect": 86.48648648648648,
        "depth_ok": 100,
        "unique_title": 86.48648648648648,
        "unique_metadescription": 85.71428571428571,
        "unique_h1": 100,
        "unique_content": 100
      }
    }
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 2}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
weekstringThe Monday, YYYY-MM-DD.
scorenumber or nullAverage SEO score of the pages, 0-100.
scoresobjectAverage score per category (indexability, content, mobile, technical, serps, social), 0-100 or null.
speed_scorenumber or nullAverage speed score.
errorsinteger or nullFailing rules of severity error, over all pages.
warningsinteger or nullOf severity warning.
noticesinteger or nullOf severity notice.
audit_scorenumber or nullThe audit score as in scan.summary; null for weeks before 29 October 2026.
rulesobjectRule key → share of checked pages that pass, 0-100.

scan.rules

POST /rest/v1/scan.rules · MCP tool get_scan_rules

Returns every rule of the SEO scan with its texts in English or Dutch: title, severity, category, what passing and failing mean, the problem, why it matters, how to fix it and the effort, and whether the rule is hidden for this site. Also the categories and the duplicate checks.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
langone of en, nlThe language of the texts.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.rules \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","lang":"en"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items. Texts in Dutch in this example; lang en gives English.

{
  "data": {
    "categories": [
      {
        "key": "all",
        "title": "Alle issues",
        "description": "Een overzicht van alles mogelijke site issues."
      },
      {
        "key": "content",
        "title": "Inhouds issues",
        "description": "Bekijk issues met de inhoud en tekst van de pagina."
      }
    ],
    "duplicate_checks": [
      {
        "key": "h1",
        "title": "Dubbele headings (h1)",
        "description": "Deze heading komt op meer dan 1 pagina voor. Klik voor meer informatie."
      },
      {
        "key": "title",
        "title": "Dubbele titels",
        "description": "Deze titel komt op meer dan 1 pagina voor. Klik voor meer informatie."
      }
    ],
    "rules": [
      {
        "key": "title_change",
        "title": "Titel aangepast",
        "severity": null,
        "category": "changes",
        "weight": null,
        "site_wide": false,
        "depends_on": [],
        "pass_text": null,
        "fail_text": null,
        "help": null,
        "problem": null,
        "why": null,
        "fix": null,
        "effort": null,
        "hidden": false
      },
      {
        "key": "status_change",
        "title": "Pagina status aangepast",
        "severity": null,
        "category": "changes",
        "weight": null,
        "site_wide": false,
        "depends_on": [],
        "pass_text": null,
        "fail_text": null,
        "help": null,
        "problem": null,
        "why": null,
        "fix": null,
        "effort": null,
        "hidden": false
      }
    ]
  },
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
categoriesarrayThe categories of rules.
categories[].keystringall, indexability, content, mobile, technical, serps, social, …
categories[].titlestring or nullIts name.
categories[].descriptionstring or nullWhat it covers.
duplicate_checksarrayThe duplicate checks (pages with the same title, H1, description).
duplicate_checks[].keystringtitle, h1, metadescription, …
duplicate_checks[].titlestring or nullIts name.
duplicate_checks[].descriptionstring or nullWhat it means.
rulesarrayOne per rule.
rules[].keystringThe rule key, as in rules of scan.pages.
rules[].titlestring or nullThe name of the rule.
rules[].severitystring or nullerror, warning or notice; null for rules that only record something (changes, information).
rules[].categorystring or nullThe category key.
rules[].weightnumber or nullIts weight in the page score.
rules[].site_widebooleanWhether it compares pages with each other (duplicates, orphans, broken links) instead of one page.
rules[].depends_onarray<string>Rules that must pass before this one counts.
rules[].pass_textstring or nullWhat it means when the rule passes.
rules[].fail_textstring or nullWhat it means when it fails.
rules[].helpstring or nullLonger explanation.
rules[].problemstring or nullThe problem in one line, for overviews.
rules[].whystring or nullWhy it matters.
rules[].fixstring or nullHow to fix it.
rules[].effortstring or nullHow much work fixing is: low, medium or high.
rules[].hiddenbooleanWhether the rule is hidden for this site.

scan.ai_bots

POST /rest/v1/scan.ai_bots · MCP tool get_scan_ai_bots

Returns which AI crawlers (ChatGPT, Claude, Perplexity, Gemini, …) may read the site: per crawler whether robots.txt blocks the homepage, on how many pages it is blocked and whether the server refuses it; the pages with a blocked crawler (up to 500, search crawlers first); and the AI signals per page (how much the text reads like stock AI writing, up to 500, highest first). checked_at is null when the crawlers were never checked; then bots and pages are empty.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.ai_bots \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": {
    "checked_at": "2026-11-05T12:45:04Z",
    "conclusive": true,
    "can_refresh": true,
    "bots": [
      {
        "bot": "OAI-SearchBot",
        "owner": "OpenAI (ChatGPT search)",
        "type": "search",
        "blocked_on_homepage": false,
        "blocked_pages": 0,
        "server_checked": true,
        "server_blocked": false,
        "server_status": null
      },
      {
        "bot": "ChatGPT-User",
        "owner": "OpenAI (ChatGPT)",
        "type": "search",
        "blocked_on_homepage": false,
        "blocked_pages": 0,
        "server_checked": true,
        "server_blocked": false,
        "server_status": null
      }
    ],
    "pages": 43,
    "search_blocked_pages": 0,
    "training_blocked_pages": 0,
    "blocked_pages_list": [],
    "ai_content": {
      "enabled": true,
      "high": 0,
      "medium": 3,
      "low": 36,
      "too_short": 4,
      "unsupported": 0,
      "pending": 0,
      "checked": 39,
      "high_pct": 0,
      "pages": [
        {
          "page_id": "6ae33f2c6edabf178406fd95",
          "url": "https://www.example-site.nl/blog/min-doulatas",
          "score": 59,
          "level": "medium",
          "words": 563,
          "language": "nl",
          "top_signals": ["paragraph_shape", "phrases"]
        },
        {
          "page_id": "6ae33f2c6edabf178406fd91",
          "url": "https://www.example-site.nl/blog/summertime",
          "score": 37,
          "level": "medium",
          "words": 408,
          "language": "nl",
          "top_signals": ["paragraph_shape", "uniform_reading"]
        }
      ]
    }
  },
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
checked_atstring or nullWhen the crawlers were last checked, ISO 8601 UTC; null when never.
conclusivebooleanWhether the server was also checked (not only robots.txt).
can_refreshbooleanWhether you may check again now (scan.ai_bots_refresh).
botsarrayOne per crawler.
bots[].botstringThe crawler, e.g. GPTBot.
bots[].ownerstringWho runs it.
bots[].typestringsearch or training.
bots[].blocked_on_homepagebooleanWhether robots.txt blocks it from the homepage.
bots[].blocked_pagesintegerPages robots.txt blocks it from.
bots[].server_checkedbooleanWhether we also asked the server as this crawler.
bots[].server_blockedbooleanWhether the server refused it.
bots[].server_statusinteger or nullThe HTTP status the server gave it; null when it was not refused or not checked.
pagesinteger or nullScanned pages; null when never checked.
search_blocked_pagesinteger or nullPages where at least one search crawler is blocked.
training_blocked_pagesinteger or nullPages where at least one training crawler is blocked.
blocked_pages_listarrayThe pages with a blocked crawler, up to 500.
blocked_pages_list[].page_idstring or nullThe page id.
blocked_pages_list[].urlstringThe page.
blocked_pages_list[].search_blockedbooleanWhether a search crawler is blocked.
blocked_pages_list[].blockedarrayThe blocked crawlers.
blocked_pages_list[].blocked[].botstringThe crawler.
blocked_pages_list[].blocked[].typestringsearch or training.
blocked_pages_list[].blocked[].bystringrobots.txt, meta or server.
ai_contentobjectThe AI signals of the pages.
ai_content.enabledbooleanWhether AI signals are on for this site (rule ai_content_signals); when off the numbers are 0 and pages is empty.
ai_content.highintegerPages that read like stock AI writing.
ai_content.mediumintegerPages in between.
ai_content.lowintegerPages that read like a person.
ai_content.too_shortintegerPages with too little text to tell.
ai_content.unsupportedintegerPages in another language than Dutch or English.
ai_content.pendingintegerPages not checked yet.
ai_content.checkedintegerhigh + medium + low.
ai_content.high_pctnumber or nullhigh as a share of checked, 0-100; null when none were checked.
ai_content.pagesarrayChecked pages, highest score first, up to 500.
ai_content.pages[].page_idstring or nullThe page id.
ai_content.pages[].urlstringThe page.
ai_content.pages[].scorenumber or null0-100.
ai_content.pages[].levelstring or nulllow, medium or high.
ai_content.pages[].wordsinteger or nullWords in the main text.
ai_content.pages[].languagestring or nullnl or en.
ai_content.pages[].top_signalsarray<string>The three signals that counted most.

scan.clicks

POST /rest/v1/scan.clicks · MCP tool get_scan_clicks

Returns Search Console clicks of the last 28 days per address, for joining with scan.pages (address = the url without scheme, www. and trailing slash). Empty when Search Console is not connected or Search Insights is off for you; meta.search_console says which.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.clicks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items.

{
  "data": [
    {"address": "example-site.nl/zwangerschapsyoga", "clicks": 212},
    {"address": "example-site.nl", "clicks": 160}
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 2, "search_console": true}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
addressstringThe address without https://, www. and the slash at the end, e.g. example.com/blog/post.
clicksintegerClicks in 28 days up to the last complete day.

scan.link_status

POST /rest/v1/scan.link_status · MCP tool get_scan_link_status

Returns the HTTP status of internal addresses as our own scan knows them, so links to a 404 or a redirect show without fetching anything. Addresses we do not know are left out.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
urls (required)list of stringUp to 500 addresses.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.link_status \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","urls":["https://www.example-site.nl/blog/","https://www.example-site.nl/oude-pagina","https://www.example-site.nl/onbekend"]}'

Example response · Illustrative: the third address is not known to the scan, so it is left out.

{
  "data": [
    {"url": "https://www.example-site.nl/blog/", "http_status": 200},
    {"url": "https://www.example-site.nl/oude-pagina", "http_status": 404}
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 2}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
urlstringThe address as you sent it.
http_statusintegerThe status our scan got; 0 when it did not answer.

scan.views

POST /rest/v1/scan.views · MCP tool get_scan_views

Returns your saved views of the page list (named sets of filters), for this site. Every user has their own, up to 10.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.views \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · Illustrative.

{
  "data": [
    {
      "id": "3f9a1c2b7d4e",
      "name": "Blog met fouten",
      "filters": {"folder": "/blog", "severity": "error"}
    }
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 1}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
idstringThe view id.
namestringIts name.
filtersobjectThe filters as the app saved them.

scan.crawl

POST /rest/v1/scan.crawl · MCP tool

Returns started: true after starting a crawl of the whole site: the sitemap and every page it finds are scanned again. A crawl that is already running is taken over. Follow it with scan.status.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.crawl \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · The answer is always this.

{"data": {"started": true}, "meta": {"site_id": "YOUR_SITE_ID"}}

Response fields · in data:

FieldTypeWhat it is
startedbooleanAlways true.

scan.empty

POST /rest/v1/scan.empty · MCP tool

Returns deleted: true after deleting every scanned page of the site, with its links, changes and text vectors. The weekly history stays. Cannot be undone; start again with scan.crawl.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.empty \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · The answer is always this.

{"data": {"deleted": true}, "meta": {"site_id": "YOUR_SITE_ID"}}

Response fields · in data:

FieldTypeWhat it is
deletedbooleanAlways true.

scan.page_add

POST /rest/v1/scan.page_add · MCP tool

Returns the page id of an address after adding it to the scan, or of the existing page when the address is already known. Scan it with scan.page_rescan.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
url (required)stringThe full address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.page_add \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","url":"https://www.example-site.nl/nieuwe-pagina"}'

Example response · Illustrative.

{
  "data": {"page_id": "6ae33f2c6edabf178406fd99", "created": true},
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
page_idstringThe page id.
createdbooleanfalse when the page already existed.

scan.page_delete

POST /rest/v1/scan.page_delete · MCP tool

Returns deleted: true after removing one page from the scan, with its links and changes. A crawl adds it again when it is still linked or in the sitemap.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
page_id (required)string

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.page_delete \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","page_id":"6ae33f2c6edabf178406fd99"}'

Example response · Illustrative.

{
  "data": {"page_id": "6ae33f2c6edabf178406fd99", "deleted": true},
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
page_idstringThe page id.
deletedbooleanAlways true.

scan.page_save

POST /rest/v1/scan.page_save · MCP tool

Returns which fields were saved after setting what a user keeps with a page: the star, notes, and the keywords it should (not) be relevant for. Only the fields you send change.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
page_id (required)string
starredboolean
noteslist of stringThe notes (replaces them all), at most 20 kB.
keywordsstringKeywords the page should be relevant for, at most 2000 characters.
negative_keywordsstringKeywords it should not be about.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.page_save \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","page_id":"6ae33f01ec26871756010ebc","starred":true}'

Example response · Illustrative.

{
  "data": {"page_id": "6ae33f01ec26871756010ebc", "saved": ["starred"]},
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
page_idstringThe page id.
savedarray<string>The fields that were saved.

scan.rule_hide

POST /rest/v1/scan.rule_hide · MCP tool

Returns the rule and whether it is hidden, after hiding a rule for this site or showing it again. A hidden rule does not count in the scores and sits at the bottom of scan.issues.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
rule (required)stringThe rule key from scan.rules.
hidden (required)boolean

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.rule_hide \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","rule":"readability_score","hidden":true}'

Example response · Illustrative.

{
  "data": {"rule": "readability_score", "hidden": true},
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
rulestringThe rule key.
hiddenbooleanWhether it is hidden now.

scan.ai_bots_refresh

POST /rest/v1/scan.ai_bots_refresh · MCP tool

Returns the same as scan.ai_bots after checking robots.txt and the server again for every AI crawler (takes up to 30 seconds).

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.ai_bots_refresh \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID"}'

Example response · A real answer of 11 November 2026 for a small site (43 pages), anonymised; lists cut to two items. The same answer as scan.ai_bots, after checking again.

{
  "data": {
    "checked_at": "2026-11-05T12:45:04Z",
    "conclusive": true,
    "can_refresh": true,
    "bots": [
      {
        "bot": "OAI-SearchBot",
        "owner": "OpenAI (ChatGPT search)",
        "type": "search",
        "blocked_on_homepage": false,
        "blocked_pages": 0,
        "server_checked": true,
        "server_blocked": false,
        "server_status": null
      },
      {
        "bot": "ChatGPT-User",
        "owner": "OpenAI (ChatGPT)",
        "type": "search",
        "blocked_on_homepage": false,
        "blocked_pages": 0,
        "server_checked": true,
        "server_blocked": false,
        "server_status": null
      }
    ],
    "pages": 43,
    "search_blocked_pages": 0,
    "training_blocked_pages": 0,
    "blocked_pages_list": [],
    "ai_content": {
      "enabled": true,
      "high": 0,
      "medium": 3,
      "low": 36,
      "too_short": 4,
      "unsupported": 0,
      "pending": 0,
      "checked": 39,
      "high_pct": 0,
      "pages": [
        {
          "page_id": "6ae33f2c6edabf178406fd95",
          "url": "https://www.example-site.nl/blog/min-doulatas",
          "score": 59,
          "level": "medium",
          "words": 563,
          "language": "nl",
          "top_signals": ["paragraph_shape", "phrases"]
        },
        {
          "page_id": "6ae33f2c6edabf178406fd91",
          "url": "https://www.example-site.nl/blog/summertime",
          "score": 37,
          "level": "medium",
          "words": 408,
          "language": "nl",
          "top_signals": ["paragraph_shape", "uniform_reading"]
        }
      ]
    }
  },
  "meta": {"site_id": "YOUR_SITE_ID"}
}

Response fields · in data:

FieldTypeWhat it is
checked_atstring or nullWhen the crawlers were last checked, ISO 8601 UTC; null when never.
conclusivebooleanWhether the server was also checked (not only robots.txt).
can_refreshbooleanWhether you may check again now (scan.ai_bots_refresh).
botsarrayOne per crawler.
bots[].botstringThe crawler, e.g. GPTBot.
bots[].ownerstringWho runs it.
bots[].typestringsearch or training.
bots[].blocked_on_homepagebooleanWhether robots.txt blocks it from the homepage.
bots[].blocked_pagesintegerPages robots.txt blocks it from.
bots[].server_checkedbooleanWhether we also asked the server as this crawler.
bots[].server_blockedbooleanWhether the server refused it.
bots[].server_statusinteger or nullThe HTTP status the server gave it; null when it was not refused or not checked.
pagesinteger or nullScanned pages; null when never checked.
search_blocked_pagesinteger or nullPages where at least one search crawler is blocked.
training_blocked_pagesinteger or nullPages where at least one training crawler is blocked.
blocked_pages_listarrayThe pages with a blocked crawler, up to 500.
blocked_pages_list[].page_idstring or nullThe page id.
blocked_pages_list[].urlstringThe page.
blocked_pages_list[].search_blockedbooleanWhether a search crawler is blocked.
blocked_pages_list[].blockedarrayThe blocked crawlers.
blocked_pages_list[].blocked[].botstringThe crawler.
blocked_pages_list[].blocked[].typestringsearch or training.
blocked_pages_list[].blocked[].bystringrobots.txt, meta or server.
ai_contentobjectThe AI signals of the pages.
ai_content.enabledbooleanWhether AI signals are on for this site (rule ai_content_signals); when off the numbers are 0 and pages is empty.
ai_content.highintegerPages that read like stock AI writing.
ai_content.mediumintegerPages in between.
ai_content.lowintegerPages that read like a person.
ai_content.too_shortintegerPages with too little text to tell.
ai_content.unsupportedintegerPages in another language than Dutch or English.
ai_content.pendingintegerPages not checked yet.
ai_content.checkedintegerhigh + medium + low.
ai_content.high_pctnumber or nullhigh as a share of checked, 0-100; null when none were checked.
ai_content.pagesarrayChecked pages, highest score first, up to 500.
ai_content.pages[].page_idstring or nullThe page id.
ai_content.pages[].urlstringThe page.
ai_content.pages[].scorenumber or null0-100.
ai_content.pages[].levelstring or nulllow, medium or high.
ai_content.pages[].wordsinteger or nullWords in the main text.
ai_content.pages[].languagestring or nullnl or en.
ai_content.pages[].top_signalsarray<string>The three signals that counted most.

scan.view_save

POST /rest/v1/scan.view_save · MCP tool

Returns your saved views after saving a new one (a name and the filters of the page list), at most 10 per site.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
name (required)stringAt most 40 characters.
filters (required)objectThe filters, at most 2 kB.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.view_save \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","name":"Blog met fouten","filters":{"folder":"/blog","severity":"error"}}'

Example response · Illustrative.

{
  "data": [
    {
      "id": "3f9a1c2b7d4e",
      "name": "Blog met fouten",
      "filters": {"folder": "/blog", "severity": "error"}
    }
  ],
  "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 1}
}

Response fields · data is a list; each item has:

FieldTypeWhat it is
idstringThe view id.
namestringIts name.
filtersobjectThe filters as the app saved them.

scan.view_delete

POST /rest/v1/scan.view_delete · MCP tool

Returns your saved views after deleting one.

Arguments

ArgumentTypeWhat it does
site_id (required)stringThe site: its id from list_sites, or the id in the app’s address.
id (required)stringThe id from scan.views.

Example request

curl -X POST https://app.marketingtracer.com/rest/v1/scan.view_delete \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"site_id":"YOUR_SITE_ID","id":"3f9a1c2b7d4e"}'

Example response · Illustrative: the list after deleting the only view.

{"data": [], "meta": {"site_id": "YOUR_SITE_ID", "next_cursor": null, "total": 0}}

Response fields · data is a list; each item has:

FieldTypeWhat it is
idstringThe view id.
namestringIts name.
filtersobjectThe filters as the app saved them.
White label / agency

Build your own SEO dashboard on our back end. On your domain, with one key for every client site.

On a white-label domain, the REST API and the MCP server answer on your own hostname under your agency's name and logo. Agency owners and admins can create a white-label key (mtw_…, 600 calls per minute) on the MCP page that covers every client site. When clients connect Claude or ChatGPT with OAuth, they sign in on your domain in your colours and logo. You decide per role whether clients see the MCP page and can use the API and MCP.

API hubMarketingTracer SEO API MCP serverSEO MCP for Claude, ChatGPT and Cursor API & MCPGoogle Search Console MCP and API

Free 14-day trial

MarketingTracer smart A.I. tool shows you what you need to rank. Then we'll help you get it done.

  • No credit card required
  • No installation needed
  • No strings attached
Start your 14-day trial

FAQ

Something else? Ask us.

What is the SEO audit API?

The SEO audit API (scan.*) is the REST and MCP interface to MarketingTracer's on-page SEO scanner. You can read the site audit score, failing rules with Search Console clicks, folder breakdowns, every scanned page with its rules and internal links, 400 days of logged changes, AI crawler checks and AI writing signals. Over REST you can also start crawls, add or delete pages, save page notes and hide rules for a site.

Can I start a crawl or change settings through the API and the MCP?

Yes over the REST API, while the MCP server is read-only. Anyone on a site with role user or higher can call the nine POST write endpoints (scan.crawl, scan.empty, scan.page_add, scan.page_delete, scan.page_save, scan.rule_hide, scan.ai_bots_refresh, scan.view_save and scan.view_delete).

How do I create an API key, and what is the rate limit?

You create named API keys on the MCP page (/mcp) in the app and can revoke any key there at once. A personal key (mtk_…) has the same site access and role as your login and allows 60 calls per minute across the REST API and the MCP server together. White-label owners and admins can create a key (mtw_…) that works on their own domain across every client site at 600 calls per minute. The API and MCP are included in every plan at no extra cost; see the pricing page.

How do I connect the SEO scan to Claude, ChatGPT or Cursor?

You can sign in with OAuth or send an API key as a Bearer header. In Claude.ai, Claude Desktop, ChatGPT (custom connector in developer mode) and Claude Code, add https://app.marketingtracer.com/rest/mcp/server and sign in with your dashboard email and password. For Cursor or your own scripts, pass your key in Authorization: Bearer YOUR_API_KEY.

What happens when a firewall blocks the scanner?

Pages that answer with HTTP 401, 403, 429, 5xx or no response are marked blocked: true and left out of the audit score and issue counts. Our crawler identifies itself as MarketingTracer/1.0 and sends a secret per-site X-SEO-Crawler header to your site. Your site settings and the MarketingTracer bot page show the ready-made Cloudflare and AWS WAF rules. We only scan sites for verified site owners and their stakeholders.

Does the AI signals score count toward the SEO audit score?

No. The AI writing score (0 to 100 in ai_content, for English and Dutch pages) never counts toward the page or site SEO score. It indicates how much the main text reads like stock AI writing rather than proving who wrote it. Site admins can switch the ai_content_signals rule off per site.

Are rule explanations available in Dutch as well as English?

Yes. Pass "lang": "nl" or "lang": "en" to scan.rules to get every rule's title, problem description, why it matters and how to fix it in Dutch or English. The MCP tools get_scan, get_ai_check, find_opportunities and cluster_performance take the same lang parameter.