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

PlanMCPREST APIPageSpeed tests per monthPageSpeed tests per day for manual scans, MCP and REST
Freeyes, limitedyes, limitednoneone scan every 24 hours via MCP and REST, plus one scan of up to three pages in the web app
Soloyesyes600120
Agencyyesyes4,000800
Scaleyesyes12,0002,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=group answers 422 free_tier_single_url, a project_id 403 plan_required.
  • If the same URL was measured within the last hour, scan_url and POST /scans.php return that scan with "reused": true and cost 0 without using the quota.
  • Scan responses carry quota with type (rolling), scans_used, scans_limit, scans_remaining, window_hours and next_scan_at (UTC, null when a scan is possible now), and attribution (text, url): if you show the data publicly, please credit PerfDelta. GET /usage.php and get_usage return the same quota.
  • When the quota is used up the API answers 429 quota_exhausted with next_scan_at, retry_after and Retry-After. All free scans from the web app, REST and MCP share one daily capacity. When it is used up the API answers 503 free_capacity_exhausted with retry_after (REST also sends the Retry-After header) 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_changes for any measured domain, get_scan and get_report_details for your own scans, get_history (the last three scans, older_runs_stored counts the stored older ones), list_projects, get_project, list_urls read-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 answers brief_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 key tools/list only lists the free tools.
  • REST responses to free keys carry X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Quota-Scans-Remaining and, when the quota is used up, X-Quota-Next-Scan-At.
  • Upgrade hints only appear when a limit applies, always in the same form: _meta with upgrade (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_limited and MCP the JSON-RPC error -32003, both with Retry-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

EndpointReturns
GET /projects.phpprojects: 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=enprioritised 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=enwhat 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.

EndpointBody or parametersReturns
POST /projects.phpname201 with id, name
POST /urls.phpproject_id and url, or urls as a list of up to 100added (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.phpurl_id, monitoring_enabled (true or false)id, monitoring_enabled
DELETE /urls.php?url_id={id}nonedeleted with the id; completed reports remain
GET /competitors.php?project_id={id}nonecompetitors: id, domain
POST /competitors.phpproject_id, domain (domain or URL)201 with id, domain; compared from the next scan on
DELETE /competitors.php?project_id={id}&id={competitor_id}nonedeleted with the id
GET /notes.php?id={scan_id}nonenotes: your notes on a scan with id, note, created_at
POST /notes.phpscan_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.phpnote_id, noteid, note; on any plan
DELETE /notes.php?note_id={id}nonedeleted 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.

EndpointBody or parametersReturns
GET /budgets.php?scan_id={scan_id}optional url to check one page, optional fail_status=1status (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}nonethe same check for the latest completed scan of this project URL
GET /budgets.php?project_id={id}nonebudgets: 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.phpproject_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 changesthe 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.

HTTPerrorMeaning
401unauthorizedkey missing, wrong or revoked
402budget_exhaustedthe monthly budget left after the monitoring need does not cover this scan; with needed, remaining, limit, upgrade_plan, upgrade_url
402daily_limittoday's cap does not cover this scan; same details, resets at midnight
403email_unverifiedthe 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)
403plan_requiredyour plan does not include the REST API or this endpoint (free plan); with required_plan and _meta
403upgrade_requiredyour plan does not include performance budgets
422free_tier_single_urlfree plan: one URL per scan, scope=group from Solo
429quota_exhausted, notes_limit_reachedfree plan: scan quota of the rolling window used up (with next_scan_at, retry_after, quota, _meta), or too many notes on one scan
503free_capacity_exhaustedthe shared daily capacity of free scans (web app, REST and MCP) is used up, with retry_after until midnight Pacific Time
403over_url_limit, url_limit_reached, demo_blockedURL 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
403project_limit, competitor_limit, monitoring_unavailable, notes_unavailableyour plan allows no further project, no further competitor in this project, or has no monitoring or notes
404not_found, url_not_found, unknown_project_id, unknown_url_id, unknown_competitor_id, unknown_note_idunknown or not yours
405method_not_allowedwrong HTTP method
409duplicatethe competitor is already in the project
422missing_url, invalid_url, invalid_devices, invalid_scope, missing_id, missing_name, too_many_urls, invalid_domain, invalid_monitoring_enabled, missing_note, note_too_longinput missing or invalid; only public http(s) URLs can be scanned or added
429throttled, rate_limited, too_many_failed_attemptssame URL scanned a few minutes ago (with retry_at), too many requests, too many failed logins
500scan_failedthe scan could not be started for another reason; retry later or contact us
503busy, capacity_exhausted, disabledall 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

ToolParametersCostsWhat it does
scan_urlurl (string, required), devices (optional), project_id (optional), scope (optional, url or group)PageSpeed testsstarts 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_scanscan_id (string, required)freestatus, progress, scores and Core Web Vitals per page, same fields as GET /scans.php
get_usagenonefreebudget and remaining tests, same fields as GET /usage.php
get_reporturl (string, required)freelatest result of any scanned domain, same fields as GET /report.php
get_report_detailsscan_id or url_idfreethe full report of one of your scans, see below
list_projectsnonefreeyour projects as a list of id, name, url_count
get_projectproject_id (integer, required)freeone 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_fixesurl (string, required), lang (optional, en or de)freeprioritised 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_changesurl (string, required), lang (optional, en or de)freechanges since the previous scan and implemented fixes, same fields as GET /changes.php. Use it to explain a score drop
get_agent_briefscan_id (string) or url (string), lang (optional, en or de)freeMarkdown 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_urlsproject_id (integer, required)freeURLs of a project as a list with the fields of GET /urls.php
get_historyurl_id (integer, required)freescore history of a project URL, {"history": [...]} as in GET /history.php; each hash is a scan_id for get_report_details
get_monitoring_eventsproject_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_statusscan_id or url_id, optional urlfreethe performance budget check, same fields as GET /budgets.php: passed overall and per project URL; use it after get_scan reports done
set_budgetproject_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 reductionsfreethe saved budget, or with list the same fields as GET /budgets.php?project_id=
create_projectname (string, required)freecreates a project, returns id, name
add_urlsproject_id (integer, required), urls (list of strings, required)freeadds URLs to a project, added and skipped as in POST /urls.php
remove_urlurl_id (integer, required)freeremoves a URL from its project
set_monitoringurl_id (integer, required), enabled (boolean, required)freeswitches monitoring of a project URL on or off
list_competitorsproject_id (integer, required)freecompetitors of a project as a list of id, domain
add_competitorproject_id (integer, required), domain (string, required)freeadds a competitor, returns id, domain
remove_competitorproject_id (integer, required), competitor_id (integer, required)freeremoves a competitor from a project
add_notescan_id (string, required), note (string, required)freeattaches a note to a scan, same fields as POST /notes.php
update_notenote_id (integer, required), note (string, required)freechanges one of your notes
delete_notenote_id (integer, required)freedeletes 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:

HTTPcodeMeaning
200-32700body is not valid JSON
200-32600not a JSON-RPC 2.0 request, or the Mcp-Method / Mcp-Name header does not match the body
200-32601unknown method
401-32001key missing, wrong or revoked
429-32002too many failed attempts from this IP address, Retry-After in seconds
429-32003more than 60 requests per minute, Retry-After: 60
503noneMCP switched off; plain body {"error":"disabled"}

Questions or a response that does not match this page: contact form.