APICore Web Vitals
Core Web Vitals API. Real Chrome speed for your site and its competitors, straight into your own tools.
Read whole-site LCP, INP and CLS from Google's Chrome UX Report for every site in your account and up to six competitors. We keep the data ready over REST and MCP for your dashboard or client report. Every plan includes both at 60 calls per minute.
Competitor scoreboard
Rank your site against up to six competitors on phones, desktops or all devices.
104-week history
Up to two years of 28-day windows, with every domain on the same dates.
Field diagnostics
Check server TTFB, the four image LCP steps, back/forward cache use and ad load.
Paired with GA4
Your 28-day speed lined up with GA4 conversions and revenue per session.
Core Web Vitals REST endpoints and MCP tools
Call /rest/v1/<endpoint> with POST and a JSON body, or with GET and query parameters. Your personal API key is on the MCP page in the app and works for both REST and MCP. Every call is read-only and takes site_id. Pass device as desktop or all to switch from the default phone view. Timings use the 75th percentile (p75): three quarters of real Chrome visits were at least that fast.
| REST endpoint | MCP tool | What it returns |
|---|---|---|
crux.summary | get_crux_summary | Latest 28-day p75 Core Web Vitals for the site and up to six competitors, with pass status, metric ratings, 28-day change and rank. |
crux.history | get_crux_history | Up to 104 rolling 28-day windows of LCP, INP and CLS per domain on shared dates, oldest week first. |
crux.metric | get_crux_metric | One metric in detail per week: p75 by device, visit shares by rating, image LCP steps and each competitor's newest week. |
crux.diagnose | get_crux_diagnose | Per-domain breakdown of the four image LCP steps, navigation types, largest element type, server TTFB, FCP, network RTT and ad load. |
crux.visitors | get_crux_visitors | The site's 28-day speed paired with GA4 revenue, key event rate, engaged time, bounce rate and pages per session, plus 28-day block correlations. |
What crux.summary returns for a site and its competitors
Send site_id and device to get the current scoreboard for a site and the competitors tracked with it:
curl -X POST https://app.marketingtracer.com/rest/v1/crux.summary \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"site_id": "YOUR_SITE_ID", "device": "phone"}'
{
"data": {
"enabled": true,
"enabled_at": "2026-10-28T15:36:30Z",
"device": "phone",
"own_domain": "example-site.nl",
"competitors": [
"competitor-one.nl"
],
"latest_date": "2026-10-24",
"domains": [
{
"domain": "example-site.nl",
"own": true,
"origin": "https://www.example-site.nl",
"state": "ok",
"status": "pass",
"partial": false,
"rank": 1,
"lcp_ms": {
"value": 587,
"date": "2026-10-24",
"stale": false,
"previous": 838,
"change_pct": -30,
"rating": "good"
},
"inp_ms": {
"value": 78,
"date": "2026-10-10",
"stale": true,
"previous": null,
"change_pct": null,
"rating": "good"
},
"cls": {
"value": 0,
"date": "2026-10-24",
"stale": false,
"previous": 0.01,
"change_pct": 0,
"rating": "good"
}
},
{
"domain": "competitor-one.nl",
"own": false,
"origin": "https://www.competitor-one.nl",
"state": "ok",
"status": "pass",
"partial": false,
"rank": 3,
"lcp_ms": {
"value": 1526,
"date": "2026-10-24",
"stale": false,
"previous": 1512,
"change_pct": 0.9,
"rating": "good"
},
…
}
]
},
"meta": {
"site_id": "YOUR_SITE_ID"
}
}
Your own site always comes first in domains (own: true), and each tracked competitor follows with its rank in the group. Negative numbers in change_pct mean the site got faster than 28 days earlier: -30 on lcp_ms is the drop from 838 ms to 587 ms. Look at inp_ms on example-site.nl too. Google had no phone INP value in the 2026-10-24 window, so the API keeps the last measured one (2026-10-10, 78 ms) and marks it stale: true.
Show clients where they stand against the competitors they watch
An LCP of 1,500 ms means little to a client on Monday morning. They want to know if they are faster than the shops they lose orders to, and whether last month's template change paid off.
One crux.summary call puts the client's site first and up to six competitors behind it, with pass or fail, each metric's rating, the 28-day change and the rank. Add crux.history when you want up to 104 weeks of 28-day windows on shared dates. When a smaller competitor lacks phone INP or has no Chrome data at all, partial: true and state: "no_data" keep missing metrics from plotting as 0 ms.
Put field speed and GA4 conversions on the same 28-day clock
Getting developer time for speed work is easier when your report shows conversions and revenue for the same dates and device. When a site gets faster after a release, clients ask straight away whether visitors stayed longer or bought more.
In crux.visitors, each week's 28-day LCP, INP and CLS sits next to GA4 revenue per session, key event rate, engaged time, bounce rate and pages per session. Under relations you see how strongly each speed metric and visit metric moved together across 28-day blocks, plus biggest_change with the speed and GA4 numbers before and after the sharpest shift.
Settle whether a slow LCP starts at the server, the hero image or ad scripts
When mobile LCP slips on a shop or publisher site, hosting blames the frontend and developers point at third-party tags. Wire crux.diagnose into your release check, or ask Claude Code with get_crux_diagnose while you investigate.
You get the numbers behind the score for your site and every competitor: overall TTFB, FCP, network RTT and the four image LCP steps in lcp_parts (first byte, load delay, load duration and render delay). It also returns how visits arrive, including back/forward cache (instant back-button loads) and prerender, plus ad kilobytes and main-thread ad CPU time. Seeing 478 ms to first byte next to a 940 ms load delay in lcp_parts tells your developer in ten seconds to start the hero image earlier.
Compare mobile LCP diagnostics and ad load on example-site.nl with competitor-one.nl.
get_crux_diagnoseEndpoints, 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; 429 rate_limited. The machine-readable version is openapi.json.
crux.summary
POST /rest/v1/crux.summary · MCP tool get_crux_summary
Returns the Core Web Vitals of one site next to its competitors, from real Chrome visits (Google's Chrome UX Report, whole site): per domain the latest LCP, INP and CLS (75th percentile over 28 days), the value 28 days earlier, the change, the rating, whether the domain passes, and its rank among the domains. The site comes first, then its competitors.
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. |
device | one of phone, desktop, all | phone, desktop or all (all devices together). Defaults to phone. |
Example request
curl -X POST https://app.marketingtracer.com/rest/v1/crux.summary \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"site_id":"YOUR_SITE_ID","device":"phone"}'
Example response · A real answer, anonymised and shortened to two domains and three weeks.
{
"data": {
"enabled": true,
"enabled_at": "2026-10-28T15:36:30Z",
"device": "phone",
"own_domain": "example-site.nl",
"competitors": ["competitor-one.nl"],
"latest_date": "2026-10-24",
"domains": [
{
"domain": "example-site.nl",
"own": true,
"origin": "https://www.example-site.nl",
"state": "ok",
"status": "pass",
"partial": false,
"rank": 1,
"lcp_ms": {
"value": 587,
"date": "2026-10-24",
"stale": false,
"previous": 838,
"change_pct": -30,
"rating": "good"
},
"inp_ms": {
"value": 78,
"date": "2026-10-10",
"stale": true,
"previous": null,
"change_pct": null,
"rating": "good"
},
"cls": {
"value": 0,
"date": "2026-10-24",
"stale": false,
"previous": 0.01,
"change_pct": 0,
"rating": "good"
}
},
{
"domain": "competitor-one.nl",
"own": false,
"origin": "https://www.competitor-one.nl",
"state": "ok",
"status": "pass",
"partial": false,
"rank": 3,
"lcp_ms": {
"value": 1526,
"date": "2026-10-24",
"stale": false,
"previous": 1512,
"change_pct": 0.9,
"rating": "good"
},
"inp_ms": {
"value": 95,
"date": "2026-10-24",
"stale": false,
"previous": 95,
"change_pct": 0,
"rating": "good"
},
"cls": {
"value": 0,
"date": "2026-10-24",
"stale": false,
"previous": 0,
"change_pct": 0,
"rating": "good"
}
}
]
},
"meta": {"site_id": "YOUR_SITE_ID"}
}
Response fields · in data:
| Field | Type | What it is |
|---|---|---|
enabled | boolean | Whether Core Web Vitals is switched on for the site. When false, the other fields are empty. |
enabled_at | string or null | When it was switched on, ISO 8601 UTC; null when never. |
device | string | The device the numbers are for: phone, desktop or all. |
own_domain | string | The domain of the site, without www. |
competitors | array<string> | The competitor domains of the site (the shared list of the Rank tracker, Core Web Vitals and AI Insights), at most 6. |
latest_date | string or null | Last day of the newest 28-day window of any domain, YYYY-MM-DD; null without data. |
domains | array | The site first, then each competitor. |
domains[].domain | string | The domain, without www. |
domains[].own | boolean | Whether this is the site itself. |
domains[].origin | string or null | The origin Google has data for (with or without www); null when it has none. |
domains[].state | string | ok, pending (not fetched yet) or no_data (Google has no data for this origin and device). |
domains[].status | string or null | pass when LCP, INP and CLS are all good, fail when one is not; null without LCP or CLS. |
domains[].partial | boolean | Whether status leaves out INP because Google has no INP value. |
domains[].rank | integer or null | Place among the domains (1 is best): passing first, then by how many metrics are good and how close the rest is; null without data. |
domains[].lcp_ms | object or null | Largest Contentful Paint in milliseconds; null when Google has no value for it. |
domains[].lcp_ms.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
domains[].lcp_ms.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
domains[].lcp_ms.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
domains[].lcp_ms.previous | number or null | The value 28 days before; null when there is none. |
domains[].lcp_ms.change_pct | number or null | Change against previous in percent (negative is faster); null without previous. |
domains[].lcp_ms.rating | string | good (up to 2500), needs_improvement (up to 4000) or poor. |
domains[].inp_ms | object or null | Interaction to Next Paint in milliseconds; null when Google has no value for it. |
domains[].inp_ms.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
domains[].inp_ms.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
domains[].inp_ms.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
domains[].inp_ms.previous | number or null | The value 28 days before; null when there is none. |
domains[].inp_ms.change_pct | number or null | Change against previous in percent (negative is faster); null without previous. |
domains[].inp_ms.rating | string | good (up to 200), needs_improvement (up to 500) or poor. |
domains[].cls | object or null | Cumulative Layout Shift (no unit); null when Google has no value for it. |
domains[].cls.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
domains[].cls.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
domains[].cls.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
domains[].cls.previous | number or null | The value 28 days before; null when there is none. |
domains[].cls.change_pct | number or null | Change against previous in percent; 0 for a change under 0.015, null when previous is under 0.05 or missing. |
domains[].cls.rating | string | good (up to 0.1), needs_improvement (up to 0.25) or poor. |
crux.history
POST /rest/v1/crux.history · MCP tool get_crux_history
Returns LCP, INP and CLS per week for the site and each competitor, oldest week first: one item per domain, the site first. Each week is the 75th percentile of real Chrome visits over the 28 days up to its date; a value is null when Google had none that week. All domains share the same weeks.
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. |
device | one of phone, desktop, all | phone, desktop or all (all devices together). Defaults to phone. |
weeks | integer, 1 to 104, default 104 | The newest this many weeks. |
Example request
curl -X POST https://app.marketingtracer.com/rest/v1/crux.history \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"site_id":"YOUR_SITE_ID","device":"phone","weeks":3}'
Example response · A real answer, anonymised and shortened to two domains and three weeks.
{
"data": [
{
"domain": "example-site.nl",
"own": true,
"state": "ok",
"weeks": [
{"date": "2026-10-10", "lcp_ms": 807, "inp_ms": 78, "cls": 0},
{"date": "2026-10-17", "lcp_ms": 665, "inp_ms": null, "cls": 0},
{"date": "2026-10-24", "lcp_ms": 587, "inp_ms": null, "cls": 0}
]
},
{
"domain": "competitor-one.nl",
"own": false,
"state": "ok",
"weeks": [
{"date": "2026-10-10", "lcp_ms": 1533, "inp_ms": 95, "cls": 0},
{"date": "2026-10-17", "lcp_ms": 1532, "inp_ms": 95, "cls": 0},
{"date": "2026-10-24", "lcp_ms": 1526, "inp_ms": 95, "cls": 0}
]
}
],
"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 |
|---|---|---|
domain | string | The domain, without www. |
own | boolean | Whether this is the site itself. |
state | string | ok, pending (not fetched yet) or no_data (Google has no data for this origin and device). |
weeks | array | Per week, oldest first; empty without data. |
weeks[].date | string | Last day of the 28-day window, YYYY-MM-DD. |
weeks[].lcp_ms | number or null | Largest Contentful Paint in milliseconds. |
weeks[].inp_ms | number or null | Interaction to Next Paint in milliseconds. |
weeks[].cls | number or null | Cumulative Layout Shift. |
crux.metric
POST /rest/v1/crux.metric · MCP tool get_crux_metric
Returns one Core Web Vital of the site in detail: per week the 75th percentile on the chosen device and on each device, the share of good, needs-improvement and poor visits, and for LCP the four LCP parts; plus each competitor's newest week. Values null where Google had none.
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. |
metric | one of lcp, inp, cls | lcp, inp or cls. Defaults to lcp. |
device | one of phone, desktop, all | phone, desktop or all (all devices together). Defaults to phone. |
Example request
curl -X POST https://app.marketingtracer.com/rest/v1/crux.metric \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"site_id":"YOUR_SITE_ID","metric":"lcp","device":"phone"}'
Example response · A real answer, anonymised and shortened to two domains and three weeks.
{
"data": {
"enabled": true,
"enabled_at": "2026-10-28T15:36:30Z",
"device": "phone",
"own_domain": "example-site.nl",
"competitors": ["competitor-one.nl"],
"metric": "lcp",
"unit": "ms",
"latest_date": "2026-10-24",
"weeks": [
{
"date": "2026-10-10",
"p75": 807,
"p75_phone": 807,
"p75_desktop": null,
"p75_all": 838,
"good_pct": 97.4,
"needs_improvement_pct": 1.2,
"poor_pct": 1.4,
"lcp_parts": {
"ttfb_ms": null,
"load_delay_ms": null,
"load_duration_ms": null,
"render_delay_ms": null
}
},
{
"date": "2026-10-17",
"p75": 665,
"p75_phone": 665,
"p75_desktop": null,
"p75_all": 653,
"good_pct": 98.3,
"needs_improvement_pct": 0.9,
"poor_pct": 0.8,
"lcp_parts": {
"ttfb_ms": null,
"load_delay_ms": null,
"load_duration_ms": null,
"render_delay_ms": null
}
},
{
"date": "2026-10-24",
"p75": 587,
"p75_phone": 587,
"p75_desktop": 596,
"p75_all": 587,
"good_pct": 98.6,
"needs_improvement_pct": 0.8,
"poor_pct": 0.6,
"lcp_parts": {
"ttfb_ms": null,
"load_delay_ms": null,
"load_duration_ms": null,
"render_delay_ms": null
}
}
],
"competitor_weeks": [
{
"domain": "competitor-one.nl",
"date": "2026-10-24",
"p75": 1526,
"good_pct": 95.5,
"needs_improvement_pct": 3.6,
"poor_pct": 0.9
}
]
},
"meta": {"site_id": "YOUR_SITE_ID"}
}
Response fields · in data:
| Field | Type | What it is |
|---|---|---|
enabled | boolean | Whether Core Web Vitals is switched on for the site. When false, the other fields are empty. |
enabled_at | string or null | When it was switched on, ISO 8601 UTC; null when never. |
device | string | The device the numbers are for: phone, desktop or all. |
own_domain | string | The domain of the site, without www. |
competitors | array<string> | The competitor domains of the site (the shared list of the Rank tracker, Core Web Vitals and AI Insights), at most 6. |
metric | string | lcp, inp or cls. |
unit | string or null | ms for lcp and inp; null for cls (no unit). |
latest_date | string or null | The newest week, YYYY-MM-DD; null without data. |
weeks | array | Per week of the site, oldest first. |
weeks[].date | string | Last day of the 28-day window, YYYY-MM-DD. |
weeks[].p75 | number or null | The 75th percentile on device, in unit. |
weeks[].p75_phone | number or null | The 75th percentile on phones. |
weeks[].p75_desktop | number or null | The 75th percentile on desktops. |
weeks[].p75_all | number or null | The 75th percentile on all devices together. |
weeks[].good_pct | number or null | Share of visits with a good value, 0-100, on device. |
weeks[].needs_improvement_pct | number or null | Share of visits that need improvement, 0-100. |
weeks[].poor_pct | number or null | Share of visits with a poor value, 0-100. |
weeks[].lcp_parts | object or null | For lcp only, when the largest element is an image: the 75th percentile of each part in milliseconds (each part is its own percentile, so they do not add up to LCP); null otherwise. |
weeks[].lcp_parts.ttfb_ms | number or null | Time to first byte. |
weeks[].lcp_parts.load_delay_ms | number or null | From first byte until the image starts loading. |
weeks[].lcp_parts.load_duration_ms | number or null | Loading the image. |
weeks[].lcp_parts.render_delay_ms | number or null | From loaded until shown. |
competitor_weeks | array | Each competitor's newest week with a value. |
competitor_weeks[].domain | string | The domain, without www. |
competitor_weeks[].date | string | Last day of that 28-day window, YYYY-MM-DD. |
competitor_weeks[].p75 | number or null | The 75th percentile on device, in unit. |
competitor_weeks[].good_pct | number or null | Share of good visits, 0-100. |
competitor_weeks[].needs_improvement_pct | number or null | Share of visits that need improvement, 0-100. |
competitor_weeks[].poor_pct | number or null | Share of poor visits, 0-100. |
crux.diagnose
POST /rest/v1/crux.diagnose · MCP tool get_crux_diagnose
Returns what is behind the numbers, per domain (the site first, then its competitors), each from its newest week with a value: the four LCP parts, how visits arrive (navigation types such as back/forward cache and prerender), the type of the largest element, server and network timing (TTFB, FCP, round trip time) and ads. A field is null when Google 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. |
device | one of phone, desktop, all | phone, desktop or all (all devices together). Defaults to phone. |
Example request
curl -X POST https://app.marketingtracer.com/rest/v1/crux.diagnose \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"site_id":"YOUR_SITE_ID","device":"phone"}'
Example response · A real answer, anonymised and shortened to two domains and three weeks.
{
"data": [
{
"domain": "example-site.nl",
"own": true,
"state": "ok",
"lcp_parts": {
"date": "2026-08-08",
"stale": true,
"ttfb_ms": 478,
"load_delay_ms": 940,
"load_duration_ms": 104,
"render_delay_ms": 130
},
"navigation_types": null,
"lcp_resource_types": {
"date": "2026-08-08",
"stale": true,
"items": [{"key": "text", "share_pct": 87.6}, {"key": "image", "share_pct": 12.4}]
},
"ttfb_ms": {"value": 358, "date": "2026-10-24", "stale": false},
"fcp_ms": {"value": 577, "date": "2026-10-24", "stale": false},
"rtt_ms": {"value": 66, "date": "2026-10-24", "stale": false},
"ad_density_pct": {"value": 21, "date": "2026-10-24", "stale": false},
"ad_kilobytes": {"value": 1179, "date": "2026-10-24", "stale": false},
"ad_cpu_ms": {"value": 602, "date": "2026-10-03", "stale": true},
"ad_count": {"value": 0.51, "date": "2026-10-24", "stale": false}
},
{
"domain": "competitor-one.nl",
"own": false,
"state": "ok",
"lcp_parts": {
"date": "2026-10-24",
"stale": false,
"ttfb_ms": 1103,
"load_delay_ms": 545,
"load_duration_ms": 61,
"render_delay_ms": 97
},
"navigation_types": {
"date": "2026-10-24",
"stale": false,
"items": [
{"key": "navigate", "share_pct": 76.5},
{"key": "back_forward_cache", "share_pct": 11.7},
{"key": "navigate_cache", "share_pct": 5.5},
{"key": "reload", "share_pct": 3.9},
{"key": "back_forward", "share_pct": 2.1},
{"key": "prerender", "share_pct": 0.4},
{"key": "restore", "share_pct": 0}
]
},
"lcp_resource_types": {
"date": "2026-10-24",
"stale": false,
"items": [{"key": "image", "share_pct": 50.6}, {"key": "text", "share_pct": 49.4}]
},
"ttfb_ms": {"value": 1146, "date": "2026-10-24", "stale": false},
"fcp_ms": {"value": 1377, "date": "2026-10-24", "stale": false},
"rtt_ms": {"value": 59, "date": "2026-10-24", "stale": false},
"ad_density_pct": {"value": 3, "date": "2026-10-24", "stale": false},
"ad_kilobytes": {"value": 1189, "date": "2026-10-24", "stale": false},
"ad_cpu_ms": {"value": 442, "date": "2026-10-24", "stale": false},
"ad_count": {"value": 0.15, "date": "2026-10-24", "stale": false}
}
],
"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 |
|---|---|---|
domain | string | The domain, without www. |
own | boolean | Whether this is the site itself. |
state | string | ok, pending (not fetched yet) or no_data (Google has no data for this origin and device). |
lcp_parts | object or null | The four LCP parts when the largest element is an image, in milliseconds (each its own 75th percentile). |
lcp_parts.date | string | Last day of the 28-day window, YYYY-MM-DD. |
lcp_parts.stale | boolean | Whether this is an older window than the newest one. |
lcp_parts.ttfb_ms | number or null | Time to first byte. |
lcp_parts.load_delay_ms | number or null | From first byte until the image starts loading. |
lcp_parts.load_duration_ms | number or null | Loading the image. |
lcp_parts.render_delay_ms | number or null | From loaded until shown. |
navigation_types | object or null | How visits arrive. |
navigation_types.date | string | Last day of the 28-day window, YYYY-MM-DD. |
navigation_types.stale | boolean | Whether this is an older window than the newest one. |
navigation_types.items | array | Largest share first. |
navigation_types.items[].key | string | navigate, navigate_cache, reload, restore, back_forward, back_forward_cache or prerender. |
navigation_types.items[].share_pct | number | Share of visits, 0-100. |
lcp_resource_types | object or null | What the largest element is. |
lcp_resource_types.date | string | Last day of the 28-day window, YYYY-MM-DD. |
lcp_resource_types.stale | boolean | Whether this is an older window than the newest one. |
lcp_resource_types.items | array | Largest share first. |
lcp_resource_types.items[].key | string | image or text. |
lcp_resource_types.items[].share_pct | number | Share of visits, 0-100. |
ttfb_ms | object or null | Time to first byte in milliseconds; null when Google has no value for it. |
ttfb_ms.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
ttfb_ms.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
ttfb_ms.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
fcp_ms | object or null | First Contentful Paint in milliseconds; null when Google has no value for it. |
fcp_ms.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
fcp_ms.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
fcp_ms.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
rtt_ms | object or null | Round trip time of the network in milliseconds; null when Google has no value for it. |
rtt_ms.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
rtt_ms.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
rtt_ms.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
ad_density_pct | object or null | Share of the screen taken by ads, 0-100; null when Google has no value for it. |
ad_density_pct.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
ad_density_pct.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
ad_density_pct.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
ad_kilobytes | object or null | Kilobytes of ads per page; null when Google has no value for it. |
ad_kilobytes.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
ad_kilobytes.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
ad_kilobytes.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
ad_cpu_ms | object or null | Main-thread time spent on ads in milliseconds; null when Google has no value for it. |
ad_cpu_ms.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
ad_cpu_ms.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
ad_cpu_ms.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
ad_count | object or null | Ads per page; null when Google has no value for it. |
ad_count.value | number | The value (75th percentile of real Chrome visits over the 28 days up to date). |
ad_count.date | string | Last day of the 28-day window the value is from, YYYY-MM-DD. |
ad_count.stale | boolean | Whether the value is from an older window than latest_date (Google had no newer value). |
crux.visitors
POST /rest/v1/crux.visitors · MCP tool get_crux_visitors
Returns the site's own LCP, INP and CLS per week next to its GA4 visits per session over the same 28 days and device (revenue, key event rate, engagement, bounce rate, pages), and per pair of a speed metric and a visit metric how they moved together: the rank correlation of their changes over 28-day blocks and the biggest change in speed. Visit values are null without GA4.
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. |
device | one of phone, desktop, all | phone, desktop or all (all devices together). Defaults to phone. |
Example request
curl -X POST https://app.marketingtracer.com/rest/v1/crux.visitors \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"site_id":"YOUR_SITE_ID","device":"phone"}'
Example response · A real answer, anonymised and shortened to two domains and three weeks (two of the fifteen relations).
{
"data": {
"enabled": true,
"enabled_at": "2026-10-28T15:36:30Z",
"device": "phone",
"own_domain": "example-site.nl",
"competitors": ["competitor-one.nl"],
"latest_date": "2026-10-24",
"ga4": "ok",
"has_revenue": true,
"weeks": [
{
"date": "2026-10-10",
"lcp_ms": 807,
"inp_ms": 78,
"cls": 0,
"revenue_per_session": 0,
"key_event_rate_pct": 0,
"engagement_seconds": 13,
"bounce_rate_pct": 79.8,
"pages_per_session": 1.09
},
{
"date": "2026-10-17",
"lcp_ms": 665,
"inp_ms": null,
"cls": 0,
"revenue_per_session": 0,
"key_event_rate_pct": 0.01,
"engagement_seconds": 21,
"bounce_rate_pct": 74.2,
"pages_per_session": 1.21
},
{
"date": "2026-10-24",
"lcp_ms": 587,
"inp_ms": null,
"cls": 0,
"revenue_per_session": 0,
"key_event_rate_pct": 0.03,
"engagement_seconds": 31,
"bounce_rate_pct": 66.2,
"pages_per_session": 1.5
}
],
"relations": [
{
"speed_metric": "lcp_ms",
"visit_metric": "key_event_rate_pct",
"better": "higher",
"blocks": 9,
"blocks_moved": 3,
"blocks_with": 1,
"rho": -0.411,
"strength": "some",
"direction": "expected",
"biggest_change": {
"direction": "faster",
"change_pct": -30.8,
"before": {"date": "2026-08-08", "speed": 1082, "visit": 0},
"after": {"date": "2026-09-05", "speed": 749, "visit": 0}
}
},
{
"speed_metric": "lcp_ms",
"visit_metric": "pages_per_session",
"better": "higher",
"blocks": 9,
"blocks_moved": 3,
"blocks_with": 3,
"rho": -0.667,
"strength": "clear",
"direction": "expected",
"biggest_change": {
"direction": "faster",
"change_pct": -30.8,
"before": {"date": "2026-08-08", "speed": 1082, "visit": 1.3},
"after": {"date": "2026-09-05", "speed": 749, "visit": 1.53}
}
}
]
},
"meta": {"site_id": "YOUR_SITE_ID"}
}
Response fields · in data:
| Field | Type | What it is |
|---|---|---|
enabled | boolean | Whether Core Web Vitals is switched on for the site. When false, the other fields are empty. |
enabled_at | string or null | When it was switched on, ISO 8601 UTC; null when never. |
device | string | The device the numbers are for: phone, desktop or all. |
own_domain | string | The domain of the site, without www. |
competitors | array<string> | The competitor domains of the site (the shared list of the Rank tracker, Core Web Vitals and AI Insights), at most 6. |
latest_date | string or null | The newest week, YYYY-MM-DD; null without data. |
ga4 | string or null | ok, connect (GA4 not connected) or error (Google returned an error); null when Core Web Vitals is off. |
has_revenue | boolean | Whether the site measures revenue in GA4. |
weeks | array | Per week, oldest first. |
weeks[].date | string | Last day of the 28-day window, YYYY-MM-DD. |
weeks[].lcp_ms | number or null | Largest Contentful Paint in milliseconds. |
weeks[].inp_ms | number or null | Interaction to Next Paint in milliseconds. |
weeks[].cls | number or null | Cumulative Layout Shift. |
weeks[].revenue_per_session | number or null | GA4 revenue per session, in the currency of the GA4 property. |
weeks[].key_event_rate_pct | number or null | Share of sessions with a key event, 0-100. |
weeks[].engagement_seconds | integer or null | Engaged time per session in seconds. |
weeks[].bounce_rate_pct | number or null | Share of sessions that were not engaged, 0-100. |
weeks[].pages_per_session | number or null | Page views per session. |
relations | array | One per speed metric and visit metric; empty without GA4. |
relations[].speed_metric | string | lcp_ms, inp_ms or cls. |
relations[].visit_metric | string | revenue_per_session, key_event_rate_pct, engagement_seconds, bounce_rate_pct or pages_per_session. |
relations[].better | string | higher or lower: which way the visit metric is better. |
relations[].blocks | integer | Pairs of 28-day blocks, 28 days apart, with both values. |
relations[].blocks_moved | integer | Of those, the blocks where speed changed noticeably. |
relations[].blocks_with | integer | Of those, the blocks where the visit metric moved the better way when speed got faster (or the worse way when slower). |
relations[].rho | number or null | Spearman rank correlation of the changes, -1 to 1; null with too few blocks or no movement. |
relations[].strength | string | too_few (under 6 blocks), none, some or clear. |
relations[].direction | string or null | expected (faster goes with better visits) or opposite; null without strength. |
relations[].biggest_change | object or null | The biggest change in speed between two weeks 28 days apart, with the visit metric before and after; null when there is none. |
relations[].biggest_change.direction | string | faster or slower. |
relations[].biggest_change.change_pct | number | Change in speed in percent. |
relations[].biggest_change.before | object | The week before. |
relations[].biggest_change.before.date | string | YYYY-MM-DD. |
relations[].biggest_change.before.speed | number | The speed metric. |
relations[].biggest_change.before.visit | number | The visit metric. |
relations[].biggest_change.after | object | The week after. |
relations[].biggest_change.after.date | string | YYYY-MM-DD. |
relations[].biggest_change.after.speed | number | The speed metric. |
relations[].biggest_change.after.visit | number | The visit metric. |
White-label Core Web Vitals API. Your hostname returns client LCP, INP and CLS.
When your agency runs MarketingTracer on a custom domain, the REST API and the MCP server answer on that domain under your own name. Your personal key reads every client site you can see, so a single integration feeds all your client reports. You decide whether your clients also get the API and the MCP. The whole dashboard is white label in every plan.
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
Core Web Vitals API questions
Something else? Ask us.
What data does the Core Web Vitals API return?
Real-user field data from Google's Chrome UX Report (CrUX) for the whole site (origin) and up to six tracked competitors. Each value is the 75th percentile of real Chrome visits over a rolling 28-day window on phones, desktops or all devices together. Across the five endpoints you get LCP, INP, CLS, server TTFB, FCP, network RTT, navigation types and ad load.
Does the API return Core Web Vitals per page or for the whole site?
Whole-site (origin) data only. For individual URLs, call get_scan on the MarketingTracer MCP server to read the per-page lab timings from our SEO audit. If you need field data from the site's own visitors, use CoreDash, real user monitoring from the same team. You get Core Web Vitals per page, per element and per day.
How does the API handle domains with missing Chrome UX Report data?
Missing values return null, and explicit status fields tell you why. When Google has too little Chrome traffic for a domain and device, state is no_data (or pending right after a domain is added). Domains with LCP and CLS but no INP value return partial: true in crux.summary. If a metric has no value in the newest 28-day window, crux.summary and crux.diagnose return the last measured window with its date and stale: true.
Why does enabled return false on a newly added site?
Core Web Vitals starts switched off on each site in MarketingTracer. Anyone with edit rights on the site can open Speed in the app and click Switch on to pull 40 weeks of history for the site and its competitors right away. Over time, crux.history returns up to 104 stored weeks. Every public API and MCP call is read-only, so you switch it on once in the dashboard.
How do I use the Core Web Vitals tools in Claude Code or Cursor?
Connect your MCP client over HTTP to /rest/mcp/server on app.marketingtracer.com and send your personal key in the Authorization bearer header. Your key is on the MCP page in the app. Claude Code and Cursor work right away, and the Claude app works where custom connectors support request headers (beta). ChatGPT needs a sign-in flow and does not connect. Copy-paste setup snippets are on the SEO MCP page.
Does crux.visitors require Google Analytics 4?
Yes, for the session numbers and correlations. Once GA4 is connected to the site in MarketingTracer, crux.visitors returns ga4: "ok" and pairs your speed history with GA4 session metrics (including revenue per session when has_revenue is true). When GA4 is not connected, the endpoint still returns the 28-day speed series with ga4: "connect" and an empty relations array.
What is the rate limit, and what does API access cost?
60 calls per minute per personal API key, shared across the REST API and the MCP server. Both are included in every MarketingTracer plan at no extra cost. See the pricing page for plan details.