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.

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 endpoint | MCP tool | What it does |
|---|---|---|
scan.status | get_scan_status | Whether the site was never crawled (not_started), is crawling (crawling) or finished (done), and whether maintenance is active. |
scan.summary | get_scan_summary | Audit 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.issues | get_scan_issues | Failing 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.folders | get_scan_folders | Up to eight top-level URL folders (/blog, /products, …) with scanned pages, average SEO score, errors, blocked pages and 28-day Search Console clicks. |
scan.pages | get_scan_pages | Every 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.page | get_scan_page | One 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.changes | get_scan_changes | Changes 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.history | get_scan_history | Weekly 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.rules | get_scan_rules | Every 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_bots | get_scan_ai_bots | Per 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.clicks | get_scan_clicks | Search Console clicks of the last 28 days per normalised address, for joining with scan.pages. |
scan.link_status | get_scan_link_status | HTTP status of up to 500 internal addresses as the scan last saw them. |
scan.views | get_scan_views | Your saved page-list filter views for the site (up to 10 per user). |
scan.crawl | REST 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.empty | REST only (POST) | Delete every scanned page of the site with its links, changes and text vectors. Weekly site history stays intact. |
scan.page_add | REST only (POST) | Add a full URL to the scan and return its page_id. |
scan.page_delete | REST only (POST) | Remove one page (page_id) with its links and changes from the scan. |
scan.page_save | REST only (POST) | Save user fields on a page (page_id): starred, notes, keywords and negative_keywords. |
scan.rule_hide | REST 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_refresh | REST only (POST) | Check robots.txt and the server again for every AI crawler and return the updated scan.ai_bots result. |
scan.view_save | REST only (POST) | Save a named filter view of the page list (up to 10 per user per site). |
scan.view_delete | REST 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 tool | What it returns |
|---|---|
get_scan | Issue 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_opportunities | Pages worth fixing first, ranked by optimisation_potential and joined with Search Console clicks, impressions and average position. |
page_brief | Search Console queries, click trend, Google Analytics 4 traffic and on-page scan issues for one URL. |
diagnose_traffic_drop | Compares 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_performance | Performance 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_check | AI 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
state | string | not_started (the site was never crawled), crawling (a crawl is running) or done. |
maintenance | boolean | Whether the scan is paused for maintenance (crawls and live scans wait). |
maintenance_until | string or null | Until 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
crawl | object | The crawl, as in scan.status. |
crawl.state | string | not_started (the site was never crawled), crawling (a crawl is running) or done. |
crawl.maintenance | boolean | Whether the scan is paused for maintenance (crawls and live scans wait). |
crawl.maintenance_until | string or null | Until when, ISO 8601 UTC, when known. |
pages | integer | Scanned pages. |
blocked_pages | integer | Pages the server refused (firewall); left out of every number. |
score | integer or null | The audit score, 0-100: the share of passed checks over all visible rules with a severity; null without scanned pages. |
previous_score | integer or null | The same score in the week we compare with; null when not known. |
compared_week | string or null | The week we compare with (its Monday, YYYY-MM-DD); null with less than two weeks of history. |
last_scan_date | string or null | The day the last page was scanned, YYYY-MM-DD. |
search_console | boolean | Whether Search Console clicks are used (connected, and Search Insights is on for you). |
open | object | Failing page × rule combinations per severity. |
open.errors | integer | Severity error. |
open.warnings | integer | Severity warning. |
open.notices | integer | Severity notice. |
new | object | New and back-again problems since the compared week per severity. |
new.errors | integer | Severity error. |
new.warnings | integer | Severity warning. |
new.notices | integer | Severity notice. |
fixed | object | Problems fixed since the compared week per severity. |
fixed.errors | integer | Severity error. |
fixed.warnings | integer | Severity warning. |
fixed.notices | integer | Severity notice. |
score_trend | array | The audit score per week, the last 12 weeks. |
score_trend[].week | string | The Monday, YYYY-MM-DD. |
score_trend[].score | integer or null | The audit score that week. |
trend_weeks | array<string> | The weeks of the trend in scan.issues (Mondays, YYYY-MM-DD); the trend has one more value: now. |
start_here | array<string> | The rule keys that gain most when fixed, up to three. |
site_checks | object or null | Checks of the site itself, null when never checked. |
site_checks.checked_at | string or null | When, ISO 8601 UTC. |
site_checks.checks | array | One per check. |
site_checks.checks[].key | string | https_redirect, www_redirect, hsts, ssl_valid, ssl_expiry, robots_txt, llms_txt. |
site_checks.checks[].severity | string | error, warning, notice or info. |
site_checks.checks[].ok | boolean | Whether it passes. |
site_checks.checks[].value | string or null | What 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
rule | string | The rule key (see scan.rules). |
category | string | indexability, content, mobile, technical, serps, social, … |
severity | string | error, warning or notice. |
open_pages | integer | Pages where the rule fails. |
checked_pages | integer | Pages where the rule was checked. |
new | integer or null | Pages where it started failing since the compared week; null without one. |
fixed | integer or null | Pages where it was fixed since then. |
regressed | integer or null | Pages where it fails again after having been fixed before. |
clicks | integer or null | Search Console clicks (28 days) on the failing pages; null without Search Console. |
trend | array or null | Failing 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. |
impact | number | How much fixing the rule gains, for sorting (higher first); 0 for a hidden rule. |
hidden | boolean | Whether the rule is hidden for this site (scan.rule_hide); it then does not count in the score. |
new_pages | array<string> | Pages where the rule became a problem since the compared week. |
regressed_pages | array<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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
path | string | The folder, e.g. /blog; / for pages in the root. |
pages | integer | Scanned pages in it. |
score | integer or null | Average SEO score of its HTML pages; null when there are none. |
errors | integer | Failing rules of severity error, summed over its pages. |
blocked_pages | integer | Pages the server refused. |
clicks | integer or null | Search 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
rule | string | Only pages where this rule fails (a key from scan.rules). |
limit | integer, 1 to 5,000, default 1,000 | |
cursor | string | meta.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:
| Field | Type | What it is |
|---|---|---|
id | string | The page id (page_id elsewhere). |
url | string | The address of the page. |
depth | integer or null | Clicks from the homepage (0 = the homepage); null when not known. |
in_sitemap | boolean | Whether the address is in the XML sitemap. |
scanned_at | string or null | When the page was last scanned, ISO 8601 UTC; null when it was found but not scanned yet. |
http_status | integer or null | The HTTP status the page answered with; 0 when it did not answer; null before the first scan. |
type | string or null | page (an HTML page), redirect, or the kind of file; null before the first scan. |
content_type | string or null | The content type: text, image, pdf, … |
redirect_url | string or null | Where a redirect points to; null when it is no redirect. |
language | string or null | The language the page says it is in (html lang). |
country | string or null | The country the page says it is for, when it says so. |
indexable | boolean or null | Whether Google may index the page; null before the first scan. |
not_indexable | array or null | Why the page is not indexable; empty when it is; null for pages scanned before 29 October 2026. |
not_indexable[].kind | string | status (the HTTP status), type (not HTML), meta (robots meta tag), canonical (points elsewhere), robots (robots.txt), header (X-Robots-Tag). |
not_indexable[].value | string or null | What we found: the status code, the meta value, where the canonical points, the robots.txt rule. |
blocked | boolean | Whether 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. |
score | number or null | The SEO score of the page, 0-100; null before the first scan. |
scores | object | The score per category, 0-100 or null. |
scores.indexability | number or null | Can Google find and index the page. |
scores.content | number or null | Headings, text, images. |
scores.mobile | number or null | How the page works on a phone. |
scores.technical | number or null | Speed, links, files, security. |
scores.serps | number or null | How the page shows in Google: title, description. |
scores.social | number or null | Sharing on social media. |
errors | integer | Failing rules of severity error on this page. |
warnings | integer | Failing rules of severity warning. |
notices | integer | Failing rules of severity notice. |
rules | object | Every rule checked on this page: rule key (see scan.rules) → true (passes) or false (fails). Site-wide checks (duplicate titles, orphan pages, …) are included. |
title | string | The title tag. |
meta_description | string | The meta description. |
h1 | string | The first H1. |
first_paragraph | string | The first paragraph of the main text. |
serp_title | string | The title as Google will probably show it (cut to fit). |
serp_description | string | The description as Google will probably show it. |
serp_url | string or null | The address as it appears in Google (the canonical address when the page has one). |
speed_score | number or null | The speed score of the scan, 0-100. |
timing_ms | object or null | The 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. |
opi | number | Optimisation potential: how much the page would gain from fixing its problems, weighed by its importance; higher = fix first. |
pagerank | number | Internal PageRank, 0-100: how strong the page is in the internal link structure. |
starred | boolean | Whether someone starred the page. |
inlinks | object | Internal links to this page, per place on the linking page. |
inlinks.total | integer | All internal links to the page. |
inlinks.main | integer | In the main text. |
inlinks.nav | integer | In the menu. |
inlinks.footer | integer | In the footer. |
duplicate_of | array<string> | Pages with (almost) the same text (rule unique_content), up to 5 addresses; empty when there are none. |
ai_content | object or null | AI signals: how much the main text reads like stock AI writing (Dutch and English only); null when not checked. |
ai_content.version | integer or null | Version of the scoring. |
ai_content.language | string or null | Language of the text: nl or en. |
ai_content.words | integer or null | Words in the main text. |
ai_content.score | number or null | 0-100: how much the text reads like stock AI writing. |
ai_content.level | string or null | low, medium, high, too_short or unsupported. |
ai_content.signals | array | The signals that counted most. |
ai_content.signals[].key | string | The signal, e.g. phrases, em_dash, triplets. |
ai_content.signals[].count | number or null | How often it occurs (weighted: some signals count half). |
ai_content.signals[].points | number or null | What it adds to the score. |
ai_content.paragraphs | object or null | Paragraphs per level. |
ai_content.paragraphs.high | integer | Paragraphs that read like AI. |
ai_content.paragraphs.medium | integer | Paragraphs in between. |
ai_content.paragraphs.low | integer | Paragraphs that read like a person. |
ai_content.reading_score | number or null | Reading ease (Flesch-Douma for nl, Flesch for en). |
ai_content.checked_at | string or null | When it was checked, ISO 8601 UTC. |
notes | array | Notes people added to the page. |
keywords | string | The keywords the page should be relevant for (set by the user; used by the relevance rules). |
negative_keywords | string | Keywords 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
page_id | string | The page id from scan.pages. |
url | string | Or 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:
| Field | Type | What it is |
|---|---|---|
id | string | The page id (page_id elsewhere). |
url | string | The address of the page. |
depth | integer or null | Clicks from the homepage (0 = the homepage); null when not known. |
in_sitemap | boolean | Whether the address is in the XML sitemap. |
scanned_at | string or null | When the page was last scanned, ISO 8601 UTC; null when it was found but not scanned yet. |
http_status | integer or null | The HTTP status the page answered with; 0 when it did not answer; null before the first scan. |
type | string or null | page (an HTML page), redirect, or the kind of file; null before the first scan. |
content_type | string or null | The content type: text, image, pdf, … |
redirect_url | string or null | Where a redirect points to; null when it is no redirect. |
language | string or null | The language the page says it is in (html lang). |
country | string or null | The country the page says it is for, when it says so. |
indexable | boolean or null | Whether Google may index the page; null before the first scan. |
not_indexable | array or null | Why the page is not indexable; empty when it is; null for pages scanned before 29 October 2026. |
not_indexable[].kind | string | status (the HTTP status), type (not HTML), meta (robots meta tag), canonical (points elsewhere), robots (robots.txt), header (X-Robots-Tag). |
not_indexable[].value | string or null | What we found: the status code, the meta value, where the canonical points, the robots.txt rule. |
blocked | boolean | Whether 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. |
score | number or null | The SEO score of the page, 0-100; null before the first scan. |
scores | object | The score per category, 0-100 or null. |
scores.indexability | number or null | Can Google find and index the page. |
scores.content | number or null | Headings, text, images. |
scores.mobile | number or null | How the page works on a phone. |
scores.technical | number or null | Speed, links, files, security. |
scores.serps | number or null | How the page shows in Google: title, description. |
scores.social | number or null | Sharing on social media. |
errors | integer | Failing rules of severity error on this page. |
warnings | integer | Failing rules of severity warning. |
notices | integer | Failing rules of severity notice. |
rules | object | Every rule checked on this page: rule key (see scan.rules) → true (passes) or false (fails). Site-wide checks (duplicate titles, orphan pages, …) are included. |
title | string | The title tag. |
meta_description | string | The meta description. |
h1 | string | The first H1. |
first_paragraph | string | The first paragraph of the main text. |
serp_title | string | The title as Google will probably show it (cut to fit). |
serp_description | string | The description as Google will probably show it. |
serp_url | string or null | The address as it appears in Google (the canonical address when the page has one). |
speed_score | number or null | The speed score of the scan, 0-100. |
timing_ms | object or null | The 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. |
opi | number | Optimisation potential: how much the page would gain from fixing its problems, weighed by its importance; higher = fix first. |
pagerank | number | Internal PageRank, 0-100: how strong the page is in the internal link structure. |
starred | boolean | Whether someone starred the page. |
inlinks | object | Internal links to this page, per place on the linking page. |
inlinks.total | integer | All internal links to the page. |
inlinks.main | integer | In the main text. |
inlinks.nav | integer | In the menu. |
inlinks.footer | integer | In the footer. |
duplicate_of | array<string> | Pages with (almost) the same text (rule unique_content), up to 5 addresses; empty when there are none. |
ai_content | object or null | AI signals: how much the main text reads like stock AI writing (Dutch and English only); null when not checked. |
ai_content.version | integer or null | Version of the scoring. |
ai_content.language | string or null | Language of the text: nl or en. |
ai_content.words | integer or null | Words in the main text. |
ai_content.score | number or null | 0-100: how much the text reads like stock AI writing. |
ai_content.level | string or null | low, medium, high, too_short or unsupported. |
ai_content.signals | array | The signals that counted most. |
ai_content.signals[].key | string | The signal, e.g. phrases, em_dash, triplets. |
ai_content.signals[].count | number or null | How often it occurs (weighted: some signals count half). |
ai_content.signals[].points | number or null | What it adds to the score. |
ai_content.paragraphs | object or null | Paragraphs per level. |
ai_content.paragraphs.high | integer | Paragraphs that read like AI. |
ai_content.paragraphs.medium | integer | Paragraphs in between. |
ai_content.paragraphs.low | integer | Paragraphs that read like a person. |
ai_content.reading_score | number or null | Reading ease (Flesch-Douma for nl, Flesch for en). |
ai_content.checked_at | string or null | When it was checked, ISO 8601 UTC. |
notes | array | Notes people added to the page. |
keywords | string | The keywords the page should be relevant for (set by the user; used by the relevance rules). |
negative_keywords | string | Keywords the page should not be about. |
incoming_links | array | Every internal link to this page. |
incoming_links[].from_url | string | The page that links here. |
incoming_links[].anchor | string | The link text. |
incoming_links[].places | array | Where on the linking page the link is (a page can link more than once). |
incoming_links[].places[].text | string | The link text. |
incoming_links[].places[].place | string | main (in the text), nav (menu) or footer. |
weeks | array | The score of this page per week (last 26 weeks), oldest first. |
weeks[].week | string | The Monday of the week, YYYY-MM-DD. |
weeks[].score | number or null | The SEO score that week. |
weeks[].scores | object | The score per category that week (indexability, content, mobile, technical, serps, social). |
ai_bots | array or null | Which AI crawlers may read this page; null when not checked. |
ai_bots[].bot | string | The crawler, e.g. GPTBot. |
ai_bots[].owner | string | Who runs it. |
ai_bots[].type | string | search (fetches pages to answer a question) or training (collects text to train a model). |
ai_bots[].blocked | boolean | Whether this crawler is blocked from the page. |
ai_bots[].blocked_by | string or null | What 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
page_id | string | Only this page. |
limit | integer, 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:
| Field | Type | What it is |
|---|---|---|
date | string | The day the scan saw the change, YYYY-MM-DD. |
url | string | The page. |
page_id | string or null | The page id. |
rule | string | The rule (see scan.rules), or title_change, h1_change, metadescription_change, status_change. |
kind | string | new (the rule started failing), fixed (it passes again) or changed (a value changed: title, H1, description, status). |
from | string or null | The value before, for changed; otherwise null. |
to | string or null | The 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
week | string | The Monday, YYYY-MM-DD. |
score | number or null | Average SEO score of the pages, 0-100. |
scores | object | Average score per category (indexability, content, mobile, technical, serps, social), 0-100 or null. |
speed_score | number or null | Average speed score. |
errors | integer or null | Failing rules of severity error, over all pages. |
warnings | integer or null | Of severity warning. |
notices | integer or null | Of severity notice. |
audit_score | number or null | The audit score as in scan.summary; null for weeks before 29 October 2026. |
rules | object | Rule 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
lang | one of en, nl | The 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:
| Field | Type | What it is |
|---|---|---|
categories | array | The categories of rules. |
categories[].key | string | all, indexability, content, mobile, technical, serps, social, … |
categories[].title | string or null | Its name. |
categories[].description | string or null | What it covers. |
duplicate_checks | array | The duplicate checks (pages with the same title, H1, description). |
duplicate_checks[].key | string | title, h1, metadescription, … |
duplicate_checks[].title | string or null | Its name. |
duplicate_checks[].description | string or null | What it means. |
rules | array | One per rule. |
rules[].key | string | The rule key, as in rules of scan.pages. |
rules[].title | string or null | The name of the rule. |
rules[].severity | string or null | error, warning or notice; null for rules that only record something (changes, information). |
rules[].category | string or null | The category key. |
rules[].weight | number or null | Its weight in the page score. |
rules[].site_wide | boolean | Whether it compares pages with each other (duplicates, orphans, broken links) instead of one page. |
rules[].depends_on | array<string> | Rules that must pass before this one counts. |
rules[].pass_text | string or null | What it means when the rule passes. |
rules[].fail_text | string or null | What it means when it fails. |
rules[].help | string or null | Longer explanation. |
rules[].problem | string or null | The problem in one line, for overviews. |
rules[].why | string or null | Why it matters. |
rules[].fix | string or null | How to fix it. |
rules[].effort | string or null | How much work fixing is: low, medium or high. |
rules[].hidden | boolean | Whether 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
checked_at | string or null | When the crawlers were last checked, ISO 8601 UTC; null when never. |
conclusive | boolean | Whether the server was also checked (not only robots.txt). |
can_refresh | boolean | Whether you may check again now (scan.ai_bots_refresh). |
bots | array | One per crawler. |
bots[].bot | string | The crawler, e.g. GPTBot. |
bots[].owner | string | Who runs it. |
bots[].type | string | search or training. |
bots[].blocked_on_homepage | boolean | Whether robots.txt blocks it from the homepage. |
bots[].blocked_pages | integer | Pages robots.txt blocks it from. |
bots[].server_checked | boolean | Whether we also asked the server as this crawler. |
bots[].server_blocked | boolean | Whether the server refused it. |
bots[].server_status | integer or null | The HTTP status the server gave it; null when it was not refused or not checked. |
pages | integer or null | Scanned pages; null when never checked. |
search_blocked_pages | integer or null | Pages where at least one search crawler is blocked. |
training_blocked_pages | integer or null | Pages where at least one training crawler is blocked. |
blocked_pages_list | array | The pages with a blocked crawler, up to 500. |
blocked_pages_list[].page_id | string or null | The page id. |
blocked_pages_list[].url | string | The page. |
blocked_pages_list[].search_blocked | boolean | Whether a search crawler is blocked. |
blocked_pages_list[].blocked | array | The blocked crawlers. |
blocked_pages_list[].blocked[].bot | string | The crawler. |
blocked_pages_list[].blocked[].type | string | search or training. |
blocked_pages_list[].blocked[].by | string | robots.txt, meta or server. |
ai_content | object | The AI signals of the pages. |
ai_content.enabled | boolean | Whether AI signals are on for this site (rule ai_content_signals); when off the numbers are 0 and pages is empty. |
ai_content.high | integer | Pages that read like stock AI writing. |
ai_content.medium | integer | Pages in between. |
ai_content.low | integer | Pages that read like a person. |
ai_content.too_short | integer | Pages with too little text to tell. |
ai_content.unsupported | integer | Pages in another language than Dutch or English. |
ai_content.pending | integer | Pages not checked yet. |
ai_content.checked | integer | high + medium + low. |
ai_content.high_pct | number or null | high as a share of checked, 0-100; null when none were checked. |
ai_content.pages | array | Checked pages, highest score first, up to 500. |
ai_content.pages[].page_id | string or null | The page id. |
ai_content.pages[].url | string | The page. |
ai_content.pages[].score | number or null | 0-100. |
ai_content.pages[].level | string or null | low, medium or high. |
ai_content.pages[].words | integer or null | Words in the main text. |
ai_content.pages[].language | string or null | nl or en. |
ai_content.pages[].top_signals | array<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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
address | string | The address without https://, www. and the slash at the end, e.g. example.com/blog/post. |
clicks | integer | Clicks 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
urls (required) | list of string | Up 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:
| Field | Type | What it is |
|---|---|---|
url | string | The address as you sent it. |
http_status | integer | The 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
id | string | The view id. |
name | string | Its name. |
filters | object | The 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
started | boolean | Always 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
deleted | boolean | Always 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
url (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
page_id | string | The page id. |
created | boolean | false 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
page_id | string | The page id. |
deleted | boolean | Always 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
page_id (required) | string | |
starred | boolean | |
notes | list of string | The notes (replaces them all), at most 20 kB. |
keywords | string | Keywords the page should be relevant for, at most 2000 characters. |
negative_keywords | string | Keywords 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:
| Field | Type | What it is |
|---|---|---|
page_id | string | The page id. |
saved | array<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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
rule (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
rule | string | The rule key. |
hidden | boolean | Whether 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
checked_at | string or null | When the crawlers were last checked, ISO 8601 UTC; null when never. |
conclusive | boolean | Whether the server was also checked (not only robots.txt). |
can_refresh | boolean | Whether you may check again now (scan.ai_bots_refresh). |
bots | array | One per crawler. |
bots[].bot | string | The crawler, e.g. GPTBot. |
bots[].owner | string | Who runs it. |
bots[].type | string | search or training. |
bots[].blocked_on_homepage | boolean | Whether robots.txt blocks it from the homepage. |
bots[].blocked_pages | integer | Pages robots.txt blocks it from. |
bots[].server_checked | boolean | Whether we also asked the server as this crawler. |
bots[].server_blocked | boolean | Whether the server refused it. |
bots[].server_status | integer or null | The HTTP status the server gave it; null when it was not refused or not checked. |
pages | integer or null | Scanned pages; null when never checked. |
search_blocked_pages | integer or null | Pages where at least one search crawler is blocked. |
training_blocked_pages | integer or null | Pages where at least one training crawler is blocked. |
blocked_pages_list | array | The pages with a blocked crawler, up to 500. |
blocked_pages_list[].page_id | string or null | The page id. |
blocked_pages_list[].url | string | The page. |
blocked_pages_list[].search_blocked | boolean | Whether a search crawler is blocked. |
blocked_pages_list[].blocked | array | The blocked crawlers. |
blocked_pages_list[].blocked[].bot | string | The crawler. |
blocked_pages_list[].blocked[].type | string | search or training. |
blocked_pages_list[].blocked[].by | string | robots.txt, meta or server. |
ai_content | object | The AI signals of the pages. |
ai_content.enabled | boolean | Whether AI signals are on for this site (rule ai_content_signals); when off the numbers are 0 and pages is empty. |
ai_content.high | integer | Pages that read like stock AI writing. |
ai_content.medium | integer | Pages in between. |
ai_content.low | integer | Pages that read like a person. |
ai_content.too_short | integer | Pages with too little text to tell. |
ai_content.unsupported | integer | Pages in another language than Dutch or English. |
ai_content.pending | integer | Pages not checked yet. |
ai_content.checked | integer | high + medium + low. |
ai_content.high_pct | number or null | high as a share of checked, 0-100; null when none were checked. |
ai_content.pages | array | Checked pages, highest score first, up to 500. |
ai_content.pages[].page_id | string or null | The page id. |
ai_content.pages[].url | string | The page. |
ai_content.pages[].score | number or null | 0-100. |
ai_content.pages[].level | string or null | low, medium or high. |
ai_content.pages[].words | integer or null | Words in the main text. |
ai_content.pages[].language | string or null | nl or en. |
ai_content.pages[].top_signals | array<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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
name (required) | string | At most 40 characters. |
filters (required) | object | The 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:
| Field | Type | What it is |
|---|---|---|
id | string | The view id. |
name | string | Its name. |
filters | object | The 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
| Argument | Type | What it does |
|---|---|---|
site_id (required) | string | The site: its id from list_sites, or the id in the app’s address. |
id (required) | string | The 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:
| Field | Type | What it is |
|---|---|---|
id | string | The view id. |
name | string | Its name. |
filters | object | The filters as the app saved them. |
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.
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
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.