PerfDelta
Run PageSpeed scans and read Core Web Vitals, scores and history for any public website.
Hosted MCP Server
npx add-mcp 'https://perfdelta.com/api/mcp.php'Installs into Claude Code, Codex, Cursor and more
Documentation
PageSpeed API: MCP and REST Reference
Last updated: 16 October 2026.
PerfDelta offers two ways to start PageSpeed scans programmatically, manage your projects and read the full results: MCP for AI agents such as Claude, ChatGPT or Cursor, and a REST API with plain JSON for Zapier, Make or your own dashboards. Both use the same API key and the same budget as the web app. How each value is measured is described in the methodology.
Access by plan
| Plan | MCP | REST API | PageSpeed tests per month | PageSpeed tests per day for manual scans, MCP and REST |
|---|---|---|---|---|
| Free | yes, limited | yes, limited | none | one scan every 24 hours via MCP and REST, plus one scan of up to three pages in the web app |
| Solo | yes | yes | 600 | 120 |
| Agency | yes | yes | 4,000 | 800 |
| Scale | yes | yes | 12,000 | 2,400 |
API keys are created on the API keys page of your account. Team members use the plan, projects and budget of the team owner.
Free plan
Setup and the agent workflow on the free plan are described in the article on the PerfDelta free plan.
With a free account you create one API key (after confirming your email address or signing in with Google) and use MCP and the REST API with these limits:
- One scan per rolling 24 hours, one URL per scan; mobile and desktop count as one scan. The next scan is possible 24 hours after the last counted one.
scope=groupanswers422 free_tier_single_url, aproject_id403 plan_required. - If the same URL was measured within the last hour,
scan_urlandPOST /scans.phpreturn that scan with"reused": trueandcost0 without using the quota. - Scan responses carry
quotawithtype(rolling),scans_used,scans_limit,scans_remaining,window_hoursandnext_scan_at(UTC,nullwhen a scan is possible now), andattribution(text,url): if you show the data publicly, please credit PerfDelta.GET /usage.phpandget_usagereturn the samequota. - When the quota is used up the API answers
429 quota_exhaustedwithnext_scan_at,retry_afterandRetry-After. All free scans from the web app, REST and MCP share one daily capacity. When it is used up the API answers503 free_capacity_exhaustedwithretry_after(REST also sends theRetry-Afterheader) until the daily reset at midnight Pacific Time, when Google resets the PageSpeed Insights quota. - Free without limit (apart from 30 requests per minute):
get_report,get_fixes,get_changesfor any measured domain,get_scanandget_report_detailsfor your own scans,get_history(the last three scans,older_runs_storedcounts the stored older ones),list_projects,get_project,list_urlsread-only. - The agent brief (
get_agent_brief) and notes (add_note,update_note,delete_note) work for your own scans: scans your account started in the web app, via REST or MCP, scans in your projects and analyses you ran in the same browser before signing in. A brief for another domain answersbrief_not_owned; it is included for any measured domain from the Solo plan. At most 50 notes per scan (429 notes_limit_reached). - All other tools and endpoints (project and URL changes, monitoring, competitors, performance budgets) answer
plan_required. With a free keytools/listonly lists the free tools. - REST responses to free keys carry
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,X-Quota-Scans-Remainingand, when the quota is used up,X-Quota-Next-Scan-At. - Upgrade hints only appear when a limit applies, always in the same form:
_metawithupgrade(reason,required_plan,unlocks,url). Normal responses carry no hint.
Authentication
Every request carries the API key as a bearer token:
Authorization: Bearer YOUR_API_KEY
For the Claude.ai connector, which cannot send headers, the MCP endpoint also accepts the key as a URL parameter: https://perfdelta.com/api/mcp.php?key=YOUR_API_KEY. Anyone with this URL can use your key, so keep it private and revoke the key if it leaks. The REST API accepts the header only.
REST: without a valid key the API answers 401 unauthorized. After 20 failed attempts within 15 minutes from the same IP address it answers 429 too_many_failed_attempts with Retry-After in seconds. A plan without access gets 403 plan_required with the plan that includes it (required_plan). MCP applies the same rules but answers with JSON-RPC errors, see MCP errors. Failed attempts are counted separately for MCP and REST.
Budget and costs
- A PageSpeed test is one Lighthouse run for one URL on one device. A scan of three URLs on mobile and desktop costs six.
- Scans via MCP and REST draw on the same monthly budget as monitoring and manual scans in the web app. Monitoring comes first: a scan only starts if it leaves enough for monitoring until the end of the month.
- Manual scans, including MCP and REST, have a daily cap of one fifth of the monthly budget. The daily cap resets at midnight, the monthly budget on the first of the month.
- A URL from one of your projects is scanned in its project, together with the other URLs of the same domain and interval. The cost covers all of these URLs. Your project competitors are compared with Chrome UX Report field data, which costs no PageSpeed tests.
- Reading reports, projects, URLs, history, scan status and usage is free, and so is managing projects, URLs and competitors. URLs you add are measured by monitoring from the monthly budget.
- Each account can send 60 requests per minute, counted separately for MCP and REST. All keys of the account and all team members share this limit. Above it REST answers
429 rate_limitedand MCP the JSON-RPC error-32003, both withRetry-After: 60.
The parameter devices chooses what is measured: both (default), mobile or desktop. A single device halves the cost.
REST API
Base URL: https://perfdelta.com/api/v1. All responses are JSON. Times are in German local time (Europe/Berlin) in the format YYYY-MM-DD HH:MM:SS.
Start a scan: POST /scans.php
Body as JSON or form-encoded: url (absolute, publicly reachable URL) or url_id (a URL from your projects, see /urls.php), plus optionally devices, project_id and scope. If both are sent, url wins. With project_id, a URL that is not yet in one of your projects is first added to that project, like "Add URL" on the project page; it then counts against your URL limit and the scan appears in your dashboard and project history. scope sets what is measured: url (default) measures only this URL. group measures all URLs of its project group on the same domain, like the scan button on the project page, and costs one test per URL and device; only group scans of a URL that shares its group with other URLs appear in its project history. A single-URL scan stays readable by scan_id and report_url.
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/", "devices": "both"}' \
"https://perfdelta.com/api/v1/scans.php"
Response 202:
{
"scan_id": "7c88c55c1d1bc8f6727d8d19ba9c9a00",
"status": "running",
"devices": "both",
"scope": "url",
"measured_urls": 1,
"cost": 2,
"report_url": "https://perfdelta.com/en/r/example.com/7c88c55c-1d1b-c8f6-727d-8d19ba9c9a00",
"budget_remaining": 11936,
"budget_warning": null,
"daily_remaining": 2398,
"project_id": 3,
"url_id": 41,
"added_to_project": true
}
budget_remaining is what is still left this month for manual scans, MCP and REST after the monitoring need; daily_remaining is the rest of today's cap. budget_warning is null below 80 percent of the monthly budget (counted after this scan) and on plans without a monthly budget; from there it is an object with used_pct, upgrade_plan (next plan with a larger budget, null on Scale) and upgrade_url (pricing page in your account language), the same threshold as in the dashboard. Show it to the person who pays for the plan before scans start failing with 402. project_id and url_id name the project URL the scan belongs to (null for a URL outside your projects); added_to_project is true when this request added the URL. scope repeats the measuring scope, measured_urls is the number of URLs in this scan.
Scan status and result: GET /scans.php?id={scan_id}
Returns status (running, done or error), started_at and report_url. While running it adds step, step_total, step_msg, updated_at and pages measured so far (same format as below), so a slow scan can be told apart from a stuck one and finished pages can be read early; with error it adds error. With done it also returns mobile_score, desktop_score (average lab score of the measured pages, 0 to 100, null for a device that was not measured), measured_at and pages: one entry per measured page and device with url, device, score, lab (lcp_ms, cls, fcp_ms, tbt_ms, ttfb_ms from Lighthouse) and field (lcp_ms, inp_ms, cls, fcp_ms, ttfb_ms at the 75th percentile of real Chrome users, null without enough field data). A scan usually takes one to three minutes; poll every 15 to 30 seconds. Only the account that started the scan can read it, any other key gets 404. The id may also be written with hyphens as in report_url; case does not matter.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://perfdelta.com/api/v1/scans.php?id=7c88c55c1d1bc8f6727d8d19ba9c9a00"
Budget: GET /usage.php
Fields: plan, unit (always PageSpeed tests (one per URL and device)), daily_limit, daily_used, daily_remaining, monthly_budget, monthly_used, monitoring_projected (what monitoring needs per month with the current settings), monitoring_remaining_need (what it still needs until the end of the month), available_for_manual_scans, monthly_used_pct (share of the monthly budget used, 0 to 100), upgrade_plan (next plan with more capacity, null on Scale), upgrade_url (pricing page in your account language) and budget_warning (from 80 percent of the monthly budget an object with used_pct, upgrade_plan and upgrade_url, otherwise null). The counts are PageSpeed tests. Without a monthly budget monthly_budget and budget_warning are null and the five fields from monthly_used to monthly_used_pct are omitted. At 80 and at 100 percent the account address also gets one email per month.
Reading data
| Endpoint | Returns |
|---|---|
GET /projects.php | projects: id, name |
GET /urls.php?project_id={id} | urls: id, url, monitoring_enabled, last_monitored_at, within_plan_limit (false: beyond your URL limit, not measured) |
GET /history.php?url_id={id} | history, newest first: hash, created_at, mobile_score, desktop_score |
GET /report.php?url={url} | latest completed result of any scanned domain: status, mobile_score, desktop_score, measured_at, report_url, scan_id (for /details.php), mobile_measured_at and desktop_measured_at. If the latest scan measured only one device, the other score comes from the most recent earlier scan that measured it, with its own date in mobile_measured_at or desktop_measured_at (null if that device was never measured). While a newer scan runs or after it failed also latest_status and latest_started_at. Without any completed scan, status is that of the latest scan and the scores and measured_at are null. Unknown domain: status: not_found. Only the host of url counts, a path is ignored; URL-encode the parameter. |
GET /fixes.php?url={url}&lang=en | prioritised fixes from the latest completed scan of any scanned domain, no measurement used: status, measured_at, report_url, platform (detected CMS or framework), field (source, p75, rating of LCP, INP and CLS for real users) and fixes, most important first. Each fix: id, type, resource (file or vendor the fix is about, else null), title, problem, action (text tailored to the platform, lang en or de), metrics (affected Core Web Vitals), fails_in_field (the affected metric that fails for real users, else null), basis (lab from the Lighthouse test or field from CrUX smartphone field data of the domain; field fixes since 25 October 2026: bfcache-blocked, speculation-rules, spa-initial-load, spa-route-change and LCP phases measured with real users), impact (one of estimated_savings_ms, estimated_savings_bytes, unused_bytes, main_thread_ms, transfer_bytes, total_blocking_time_ms, lcp_phase_ms, cls, affected_share, prerender_share, softnav_p75_ms with standard_p75_ms, repeat_visit_savings_ms for short cache lifetimes, http1_share), devices, pages, files (url, wasted_bytes, wasted_ms, estimated, cache_lifetime_ms for short cache lifetimes), elements (HTML of layout-shifting elements), benchmark (share of measured sites with this finding, median_ms typical saving or main-thread time there, scope, platform, domain_count), observed_effect (metric, median_delta, n, scope: median change of the lab metric where this fix disappeared between two scans, from 10 observations; since 26 October 2026 optionally good with the same fields: median change of the share of good smartphone visits, 0 to 1, of the related Core Web Vital in CrUX) and guide_url. |
GET /changes.php?url={url}&lang=en | what changed between the latest and the previous completed scan, no measurement used: status (done, no_baseline without an earlier scan with stored page data, not_found), measured_at, previous_measured_at, report_url, page and device compared, scores and previous_scores (mobile, desktop), changes (kind, direction worse, better or neutral, summary, plus resource_type, name, phase, id, before, after, bytes as applicable) and implemented (fixes from the previous scan that are gone now: title, metric, before_value, after_value, delta, since 26 October 2026 also good_metric, good_before, good_after: share of good smartphone visits of the domain, 0 to 1). |
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://perfdelta.com/api/v1/report.php?url=https%3A%2F%2Fexample.com%2F"
Full report: GET /details.php?id={scan_id}
The same response as the MCP tool get_report_details, described below: everything the report page shows its owner. id is a scan_id from /scans.php or /report.php, or a hash from /history.php; alternatively url_id returns the latest scan of a project URL. Works for scans you started and for every scan of a URL in your projects, including monitoring runs. Any other scan answers 404 not_found, a request without id or url_id 422 missing_id. Free.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://perfdelta.com/api/v1/details.php?id=7c88c55c1d1bc8f6727d8d19ba9c9a00"
Managing projects, URLs, competitors and notes
The same rules as in the web app apply: plans without multiple projects have one project, URLs count against your plan's URL limit across all projects, only public http(s) URLs are accepted, and the number of competitors per project depends on the plan. Bodies are JSON; POST also accepts form fields.
| Endpoint | Body or parameters | Returns |
|---|---|---|
POST /projects.php | name | 201 with id, name |
POST /urls.php | project_id and url, or urls as a list of up to 100 | added (id, url) and skipped (url, reason: invalid_url, duplicate or url_limit_reached); 201 if at least one URL was added, otherwise 200. URLs without a scheme get https:// |
PATCH /urls.php | url_id, monitoring_enabled (true or false) | id, monitoring_enabled |
DELETE /urls.php?url_id={id} | none | deleted with the id; completed reports remain |
GET /competitors.php?project_id={id} | none | competitors: id, domain |
POST /competitors.php | project_id, domain (domain or URL) | 201 with id, domain; compared from the next scan on |
DELETE /competitors.php?project_id={id}&id={competitor_id} | none | deleted with the id |
GET /notes.php?id={scan_id} | none | notes: your notes on a scan with id, note, created_at |
POST /notes.php | scan_id, note (at most 280 characters) | 201 with id, scan_id, note, created_at; plans with unlimited history only, the note appears in the report history |
PATCH /notes.php | note_id, note | id, note; on any plan |
DELETE /notes.php?note_id={id} | none | deleted with the id; on any plan |
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"project_id": 6, "urls": ["https://example.com/", "https://example.com/pricing/"]}' \
"https://perfdelta.com/api/v1/urls.php"
Performance budgets as a CI gate: /budgets.php
A performance budget is a limit a page must not cross, for example page weight at most 1.5 MB, JavaScript at most 400 KB, lab LCP at most 2.5 s or a lab score of at least 80. Budgets are set per project (all URLs) or per project URL, which then replaces the project budget for that URL. Each budget can carry a goal; goals are shown next to the result and never fail a check. Budgets are evaluated on values every scan already measures, so a check costs nothing beyond the scan itself. Included in every paid plan; the REST endpoint follows the REST plan rules above.
| Endpoint | Body or parameters | Returns |
|---|---|---|
GET /budgets.php?scan_id={scan_id} | optional url to check one page, optional fail_status=1 | status (done, running or no_budgets), passed (true, false, or null while running or without budgets), summary (over, ok, warn, no_data), and per project URL url_id, url, passed and budgets with metric, device, unit, direction (at_most or at_least), value, limit, goal, status (ok, warn for less than 10 percent headroom, still passing, over, no_data), goal_met, headroom and scope (project or url) plus ratchet (enabled, step, next_check). With fail_status=1 a failed check answers 409, so curl --fail stops the build without jq |
GET /budgets.php?url_id={id} | none | the same check for the latest completed scan of this project URL |
GET /budgets.php?project_id={id} | none | budgets: url_id (null for the project budget), metric, device, unit, limit, goal, source, ratchet (enabled, step: "round" or a percentage, next_check), updated_at; ratchet_log: the latest automatic reductions with url_id, metric, device, old_limit, new_limit, goal, lowered_at |
POST /budgets.php | project_id, metric, device (mobile or desktop, default mobile), limit, optional goal and url_id; delete: true removes the budget; ratchet: true lowers the limit one step toward the goal after every 30 days without a breach (needs a goal, never raises the limit, never goes below the goal or the worst value of the last 30 days), false switches it off; ratchet_step: "round" (one rounding step, default) or a percentage from 1 to 50. With ratchet and without limit only this setting of an existing budget changes | the saved budget including ratchet; 422 invalid_metric, 422 invalid_value, 422 invalid_ratchet or 422 goal_required for invalid input |
Metrics: total_kb, script_kb, image_kb, stylesheet_kb, font_kb, third_party_kb, third_party_count, request_count, perf_score (a minimum), lab_lcp_ms, tbt_ms, lab_cls, fcp_ms, ttfb_ms, and from field data (CrUX p75, mobile only) field_lcp_ms, field_inp_ms, field_cls, field_fcp_ms, field_ttfb_ms. limit and goal are numbers in the unit of the metric (KB, ms, points, count, CLS) or text with a unit such as "1.5 MB" or "2.5 s". Not to be confused with the monthly PageSpeed test budget in /usage.php.
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"project_id": 6, "metric": "script_kb", "device": "mobile", "limit": "400 KB", "goal": 300}' \
"https://perfdelta.com/api/v1/budgets.php"
A check in a deployment pipeline, for example as a GitHub Actions step after the deploy to staging: start a scan of the URL, wait until it is done, then check the budgets.
SCAN=$(curl -s -X POST -H "Authorization: Bearer $PERFDELTA_API_KEY" \
-H "Content-Type: application/json" -d '{"url": "https://staging.example.com/"}' \
"https://perfdelta.com/api/v1/scans.php" | jq -r .scan_id)
until [ "$(curl -s -H "Authorization: Bearer $PERFDELTA_API_KEY" \
"https://perfdelta.com/api/v1/scans.php?id=$SCAN" | jq -r .status)" != "running" ]; do sleep 20; done
curl --fail -s -H "Authorization: Bearer $PERFDELTA_API_KEY" \
"https://perfdelta.com/api/v1/budgets.php?scan_id=$SCAN&fail_status=1"
Error codes
Errors have the form {"error": "code"}. Errors when starting a scan, when creating or changing projects, URLs, competitors and notes, and upgrade_required add a message; scan errors may add further details as listed below.
| HTTP | error | Meaning |
|---|---|---|
| 401 | unauthorized | key missing, wrong or revoked |
| 402 | budget_exhausted | the monthly budget left after the monitoring need does not cover this scan; with needed, remaining, limit, upgrade_plan, upgrade_url |
| 402 | daily_limit | today's cap does not cover this scan; same details, resets at midnight |
| 403 | email_unverified | the email address of the account behind the key is not confirmed yet; confirm it via the link in the confirmation email (MCP answers tool calls with the same code as a tool error) |
| 403 | plan_required | your plan does not include the REST API or this endpoint (free plan); with required_plan and _meta |
| 403 | upgrade_required | your plan does not include performance budgets |
| 422 | free_tier_single_url | free plan: one URL per scan, scope=group from Solo |
| 429 | quota_exhausted, notes_limit_reached | free plan: scan quota of the rolling window used up (with next_scan_at, retry_after, quota, _meta), or too many notes on one scan |
| 503 | free_capacity_exhausted | the shared daily capacity of free scans (web app, REST and MCP) is used up, with retry_after until midnight Pacific Time |
| 403 | over_url_limit, url_limit_reached, demo_blocked | URL beyond your plan's URL limit, all URLs of your plan in use when adding one via project_id (with upgrade_plan), or the fixed demo domain |
| 403 | project_limit, competitor_limit, monitoring_unavailable, notes_unavailable | your plan allows no further project, no further competitor in this project, or has no monitoring or notes |
| 404 | not_found, url_not_found, unknown_project_id, unknown_url_id, unknown_competitor_id, unknown_note_id | unknown or not yours |
| 405 | method_not_allowed | wrong HTTP method |
| 409 | duplicate | the competitor is already in the project |
| 422 | missing_url, invalid_url, invalid_devices, invalid_scope, missing_id, missing_name, too_many_urls, invalid_domain, invalid_monitoring_enabled, missing_note, note_too_long | input missing or invalid; only public http(s) URLs can be scanned or added |
| 429 | throttled, rate_limited, too_many_failed_attempts | same URL scanned a few minutes ago (with retry_at), too many requests, too many failed logins |
| 500 | scan_failed | the scan could not be started for another reason; retry later or contact us |
| 503 | busy, capacity_exhausted, disabled | all measurement slots busy (retry shortly), daily capacity used up, API switched off |
MCP for AI agents
Endpoint: https://perfdelta.com/api/mcp.php. Transport is Streamable HTTP with JSON-RPC 2.0, stateless: every request is a single POST with a JSON body and gets a JSON answer. There is no Mcp-Session-Id and no SSE stream; GET answers 405. Supported protocol versions: 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. The server answers initialize with the version the client asks for, or with 2026-07-28 if it does not know it, and includes instructions on costs and flow.
Discovery: tools/list returns a title and annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) for every tool, so clients can ask for confirmation before remove_url, remove_competitor or delete_note. A server card (draft format) is at https://perfdelta.com/.well-known/mcp/server-card.json, the registry name is com.perfdelta/perfdelta.
initialize, notifications/initialized, ping and tools/list work without a key, so clients and directories can read the tool list before you connect. tools/call requires a key. A key that is sent but invalid is rejected for every method. If a client sends the optional headers Mcp-Method or Mcp-Name, they must match the method and tool name in the body.
An overview of what the tools do, with examples, is in the article PageSpeed MCP server.
Setup
Claude Code:
claude mcp add --transport http perfdelta https://perfdelta.com/api/mcp.php \
--header "Authorization: Bearer YOUR_API_KEY"
Or in .mcp.json of a project:
{
"mcpServers": {
"perfdelta": {
"type": "http",
"url": "https://perfdelta.com/api/mcp.php",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}
Cursor: the same block without "type" in .cursor/mcp.json. VS Code: in .vscode/mcp.json under "servers" instead of "mcpServers", with "type": "http".
Claude.ai and Claude Desktop: Settings, Connectors, Add custom connector, with the connector URL https://perfdelta.com/api/mcp.php?key=YOUR_API_KEY. ChatGPT (developer mode): Settings, Connectors, Create, with the same URL including ?key= and no authentication, because the dialog does not offer custom headers.
Tools
| Tool | Parameters | Costs | What it does |
|---|---|---|---|
scan_url | url (string, required), devices (optional), project_id (optional), scope (optional, url or group) | PageSpeed tests | starts a scan; returns the same fields as POST /scans.php. There is no url_id: pass the project URL itself. By default only that URL is measured; scope=group measures its whole project group. With project_id a new URL is added to that project first, so the report shows up in your dashboard |
get_scan | scan_id (string, required) | free | status, progress, scores and Core Web Vitals per page, same fields as GET /scans.php |
get_usage | none | free | budget and remaining tests, same fields as GET /usage.php |
get_report | url (string, required) | free | latest result of any scanned domain, same fields as GET /report.php |
get_report_details | scan_id or url_id | free | the full report of one of your scans, see below |
list_projects | none | free | your projects as a list of id, name, url_count |
get_project | project_id (integer, required) | free | one project with settings (monitoring frequency and devices, alerts, weekly report, public dashboard), competitors and urls, each with tags, monitoring state, within_plan_limit and latest_scan (scan_id, status, scores, measured_at, report_url) |
get_fixes | url (string, required), lang (optional, en or de) | free | prioritised fixes with affected files, elements, benchmark and observed effect, same fields as GET /fixes.php. Hand the files to a coding agent to fix them |
get_changes | url (string, required), lang (optional, en or de) | free | changes since the previous scan and implemented fixes, same fields as GET /changes.php. Use it to explain a score drop |
get_agent_brief | scan_id (string) or url (string), lang (optional, en or de) | free | Markdown brief for a coding agent in the website's repository: task, guardrails, baseline, all fixes with stable IDs (PD-…), files, elements and acceptance criteria, changes, budgets and how to verify. The agent reports back per fix with add_note (PD-<id> done: …); the next brief shows that status. Same content as the "Agent.md" download in the report (Solo plan and up) |
list_urls | project_id (integer, required) | free | URLs of a project as a list with the fields of GET /urls.php |
get_history | url_id (integer, required) | free | score history of a project URL, {"history": [...]} as in GET /history.php; each hash is a scan_id for get_report_details |
get_monitoring_events | project_id (integer, required), limit (optional, default 50, at most 200) | free | {"events": [...]}, newest first: kind (regression, improvement, or budget_over, budget_ok and budget_warn when a performance budget is crossed, met again or comes within 10 percent of its limit), url_id, url, scan_id, reasons (score change or Core Web Vitals failing/passing), alert_sent, created_at; kept for 90 days |
get_budget_status | scan_id or url_id, optional url | free | the performance budget check, same fields as GET /budgets.php: passed overall and per project URL; use it after get_scan reports done |
set_budget | project_id (integer, required), metric, device, limit, optional goal, url_id, delete, ratchet, ratchet_step; list: true returns all budgets of the project and the log of automatic reductions | free | the saved budget, or with list the same fields as GET /budgets.php?project_id= |
create_project | name (string, required) | free | creates a project, returns id, name |
add_urls | project_id (integer, required), urls (list of strings, required) | free | adds URLs to a project, added and skipped as in POST /urls.php |
remove_url | url_id (integer, required) | free | removes a URL from its project |
set_monitoring | url_id (integer, required), enabled (boolean, required) | free | switches monitoring of a project URL on or off |
list_competitors | project_id (integer, required) | free | competitors of a project as a list of id, domain |
add_competitor | project_id (integer, required), domain (string, required) | free | adds a competitor, returns id, domain |
remove_competitor | project_id (integer, required), competitor_id (integer, required) | free | removes a competitor from a project |
add_note | scan_id (string, required), note (string, required) | free | attaches a note to a scan, same fields as POST /notes.php |
update_note | note_id (integer, required), note (string, required) | free | changes one of your notes |
delete_note | note_id (integer, required) | free | deletes one of your notes |
Results come as one text block with JSON. Unlike REST, list_projects, list_urls and list_competitors return the list directly, without the wrapping object. Typical flow for an agent: get_usage, then scan_url, then get_scan every 15 to 30 seconds until done, then get_report_details for the full report. To set up monitoring: create_project, add_urls, optionally add_competitor.
Full report: get_report_details
Returns everything the report page shows to the owner, as JSON for analysis by an AI: summary (scores, previous scores, Core Web Vitals pages passing per device, thresholds, global pass rate, biggest bottleneck), origin_field_data_phone (CrUX for the whole domain on smartphones, the basis Google uses for mobile search: lcp_ms, inp_ms, cls, weekly history and the trend against the previous 28-day window (weeks = distance, normally 4); since 25 October 2026 also fcp_ms, ttfb_ms, dist (good/ni/poor share in percent per metric), subparts (LCP subparts of image LCPs in ms: ttfb, load_delay, load_duration, render_delay), navigation (share per navigation type), rtt (round-trip time p75 in ms), lcp_type (share of image and text LCP elements), period_end, softnav (experimental: p75 and good/ni/poor share of LCP, INP and CLS including soft navigations in single-page apps, not used for the Core Web Vitals assessment), and FCP/TTFB series in history; since 26 October 2026 also history.good (share of good visits per week, 0 to 1, for lcp, inp, cls) and, for your own domain only, navigation_history (weekly periods, back_forward share of all navigations and bfcache_hit rate, null where Chrome has no value); the same fields per competitor in competitors[].phone; null for scans before 13 October 2026 or without smartphone traffic), origin_field_data and origin_field_history (the same across all devices, kept for existing integrations; since 26 October 2026 origin_field_data.form_factors and competitors[].crux.form_factors hold the share of Chrome visits per device class: phone, desktop, tablet, 0 to 1), origin_field_data_desktop (the same as origin_field_data_phone for desktop visits, with weekly history and trend; null for older scans or without desktop traffic), competitors, perfdelta_average, industry_benchmark (plans with industry benchmark), pages, tech_stack, ai_readiness, ai_recommendations, notes and history. Each entry in pages holds one URL and device: score, field and lab with a rating per metric (good, ni, poor), field_distribution, cwv_pass, page_weight_bytes, resource_breakdown, third_parties (per provider name, main_thread_ms, bytes and benchmark: ms_median and ms_p75 of its main-thread time across all PerfDelta scans, above_p75, the most important tip and the url of the provider page; null without benchmark data), opportunities with estimated savings and diagnostics (LCP element, layout shifts, main-thread work, script bundles, largest requests; on plans with the waterfall chart also waterfall, every request in load order with url, resource_type, bytes, start_ms and end_ms, otherwise null) and delivery (since 26 October 2026: requests per HTTP protocol for first and third parties, bytes repeat visits download again because of short cache lifetimes, CSS/JS files that appear uncompressed, whether the HTML document is compressed). screenshots and filmstrip (each mobile and desktop) hold absolute image URLs, filmstrip frames with url and timing_ms. notes lists your own notes with id, note, created_at, for use with update_note and delete_note. Works for scans in your projects and for scans your account started; for other domains use get_report. Field names in detail: the top level has complete (true once the scan is done) and purged (true if the page data of an old scan was deleted). summary holds mobile_score, desktop_score, previous_mobile_score, previous_desktop_score, cwv_pages_passing (per device pass and total), cwv_thresholds, global_cwv_pass_rate (with source) and biggest_bottleneck. Each page has field_is_origin_fallback (true: values for the whole domain, the page had no own field data), field_distribution with dist shares per metric, request_count, and per opportunity id, title, savings_ms and resources; the page score is the Lighthouse performance_score. ai_recommendations_lang names the language of the recommendations. third_party_recommendations lists the providers above their benchmark p75 with name, own_ms (highest main-thread time on your pages), p75, tip and url, largest gap first. ai_readiness (null for scans before 25 October 2026) holds checks with id (search_access, bot_protection, content_without_js, snippet_controls, structured_data, server_response, agent_interaction, agent_accessibility from the Lighthouse Agentic Browsing audit, only for monitoring scans), status (pass, warn, fail, unknown) and detail, the counts passed and total (without unknown), bots (robots.txt result per AI crawler token: allowed, partial, blocked), llms_txt and ard (ai-catalog.json schema score and issues, null if Lighthouse found none).
MCP errors
Errors inside a tool come back as a normal tool result with isError: true and a message text, without the REST error code and details. Examples: unknown scan_id, project_id, url_id or competitor_id, a missing url, a reached project, URL or competitor limit, an exhausted budget, or a plan without MCP access (with the plan that includes it). When scan_url fails on budget or daily cap, the text ends with the link to the pricing page. In the free plan, tools beyond its scope and briefs for other domains return a JSON text with error (plan_required or brief_not_owned), message and required_plan, the same object as structuredContent, plus _meta with upgrade; a free scan_url answers its limits the same way. Protocol errors use JSON-RPC error codes and carry the id of the request when it can be read:
| HTTP | code | Meaning |
|---|---|---|
| 200 | -32700 | body is not valid JSON |
| 200 | -32600 | not a JSON-RPC 2.0 request, or the Mcp-Method / Mcp-Name header does not match the body |
| 200 | -32601 | unknown method |
| 401 | -32001 | key missing, wrong or revoked |
| 429 | -32002 | too many failed attempts from this IP address, Retry-After in seconds |
| 429 | -32003 | more than 60 requests per minute, Retry-After: 60 |
| 503 | none | MCP switched off; plain body {"error":"disabled"} |
Questions or a response that does not match this page: contact form.