# PageSpeed Insights & Core Web Vitals Bulk Checker (`grace_hartwell/pagespeed-cwv-monitor`) Actor

Bulk-check PageSpeed Insights scores and Core Web Vitals (LCP, INP, CLS) on mobile and desktop, with CrUX field data and regression alerts.

- **URL**: https://apify.com/grace_hartwell/pagespeed-cwv-monitor.md
- **Developed by:** [Grace M. Hartwell](https://apify.com/grace_hartwell) (community)
- **Categories:** SEO tools, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 url × strategy auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

<img src="https://apify-image-uploads-prod.s3.us-east-1.amazonaws.com/dpsKHwXKHdbwBgwOT-profile-dxtJoi0ame-avatar-hartwell-800.png" alt="Hartwell Toolworks" width="96" height="96" />

**PageSpeed Insights & Core Web Vitals Bulk Checker** runs **Google PageSpeed Insights for many URLs at once**, on **mobile and desktop**, and returns the Lighthouse performance score, **Core Web Vitals (LCP, INP, CLS)** and other speed metrics. It adds **real-user field data from the Chrome UX Report (CrUX)** and compares every result with the previous run, so it can **alert you when page speed gets worse**: a flag in the dataset plus an optional webhook. You use your own free Google API key and pay **$0.003 per URL per strategy**. Failed audits are free.

### What does the PageSpeed Insights bulk checker do?

- Calls the official **PageSpeed Insights API v5** for every URL × strategy (mobile and/or desktop).
- Returns **lab data from Lighthouse**: performance score, LCP, CLS, TBT, FCP, Speed Index and server response time (TTFB). Accessibility, best-practices and SEO scores are optional.
- Adds **CrUX field data** (p75 LCP, INP, CLS, FCP, TTFB) at URL level, falling back to origin level when the URL has too little traffic. Without CrUX, the field data PageSpeed Insights itself returns is used when available.
- Saves the latest snapshot per URL × strategy and reports **deltas compared with the previous run**.
- Raises **regression alerts** (score drop; LCP, TBT or INP increase in %; CLS increase) and optional **absolute budget alerts** (minimum score, LCP, CLS and INP budgets).
- Sends **one webhook per run** with all alerts, and only when there are alerts.

### Key features

- 📱 **Mobile and desktop** in one run.
- 🧪 **Lab and field data together**: Lighthouse lab metrics plus CrUX real-user p75 values with the collection period.
- 📉 **Regression monitoring**: each run is compared with the previous one under the same monitor name (`stateKey`).
- 🚨 **Alerts**: an `alert` flag and `alerts` array per item, plus a JSON webhook (for example to Make, Zapier, n8n or your own endpoint).
- 🔁 **Reliable API handling**: exponential backoff with jitter for 429 rate limits, 5xx Lighthouse errors and timeouts.
- 🛑 **Fails fast on key problems**: if your API key is invalid, the API is not enabled, or the key's daily quota is exhausted, the run stops with a clear message instead of failing on every URL.
- 🆓 **No key? No charge**: without an API key, audits that Google refuses are returned as free `skipped_no_api_key` items and the run still finishes successfully, telling you to add a key.
- 🔑 **Your API key stays secret**: it is stored in a secret (encrypted) input field and masked in every log and error message.

### Use cases

1. **Core Web Vitals monitoring for SEO**: schedule the Actor daily or weekly and track LCP, INP and CLS for your key pages.
2. **Release and deploy checks**: run before and after a deployment and get alerted about performance regressions.
3. **Agency and portfolio reporting**: check PageSpeed scores for many client sites or page templates in one run.
4. **Mobile vs. desktop comparison**: compare both strategies across a site's important templates.
5. **Performance budgets**: alert when LCP, CLS, INP or the performance score crosses a fixed limit.

### How much does a bulk PageSpeed Insights check cost?

The Actor uses **pay-per-event** pricing:

| Event | Price | When it is charged |
| --- | --- | --- |
| `url-audited` | **$0.003** per URL × strategy ($3.00 per 1,000 audits) | After a successful audit of one URL for one strategy |
| `apify-actor-start` | $0.00005 per run | Automatically at run start (Apify default price) |

**Free:** failed audits, invalid URLs and URLs skipped because of the budget. Google API usage is billed by Google, not by this Actor; it stays free within Google's quota for your own key.

**Example calculation: 100 URLs, mobile + desktop:**

```
100 URLs × 2 strategies       = 200 audits
200 audits × $0.003           = $0.60
+ 1 Actor start × $0.00005    = $0.00005   (default 256 MB memory → 1 start event)
------------------------------------------------
Total per run                 ≈ $0.60005
Monitoring daily for 30 days  = 30 × $0.60005 ≈ $18.00
```

### How to use the Core Web Vitals checker

1. **Get a Google API key**: in Google Cloud Console go to *APIs & Services* → *Credentials* → **Create credentials → API key**.
2. Enable the **PageSpeed Insights API** and, for field data, the **Chrome UX Report API** for that project. The input description lists the free quotas as 25,000 PSI requests per day and 150 CrUX requests per minute; check Google Cloud Console for your current limits.
3. Open the Actor in Apify Store, click **Try for free**, and add your **URLs**.
4. Paste the key into **Google API key** and choose the **Strategies** (mobile, desktop).
5. Optional: set a **Monitor name (state key)**, **Alert thresholds** and an **Alert webhook URL**.
6. Click **Start**. To monitor, create an Apify **schedule** with the same input so every run is compared with the previous one.

### Input example

```json
{
    "urls": [{ "url": "https://web.dev/" }, { "url": "https://developer.chrome.com/" }],
    "apiKey": "YOUR_GOOGLE_API_KEY",
    "strategies": ["mobile", "desktop"],
    "categories": ["seo"],
    "includeCrux": true,
    "compareWithPrevious": true,
    "stateKey": "my-site",
    "thresholds": {
        "maxScoreDrop": 5,
        "maxLcpIncreasePct": 20,
        "maxTbtIncreasePct": 30,
        "maxClsIncrease": 0.05,
        "maxInpIncreasePct": 20,
        "minPerformanceScore": 0,
        "lcpBudgetMs": 2500,
        "clsBudget": 0.1,
        "inpBudgetMs": 200
    },
    "webhookUrl": "https://example.com/my-webhook",
    "alertOnly": false,
    "maxConcurrency": 3,
    "requestTimeoutSecs": 120,
    "maxRetries": 4
}
```

| Input field | Type | Default | Description |
| --- | --- | --- | --- |
| `urls` | array | `[{ "url": "https://example.com/" }]` | Pages to audit, each once per strategy. Duplicates are removed. Replace the default with your own pages. |
| `apiKey` | string (secret) | – | Your Google API key. In practice it is required (see FAQ). Without it, refused audits are skipped for free (`skipped_no_api_key`). |
| `strategies` | array | `["mobile","desktop"]` | `mobile` and/or `desktop`. |
| `categories` | array | `[]` | Extra Lighthouse categories: `accessibility`, `best-practices`, `seo`. Performance is always included. |
| `includeCrux` | boolean | `true` | Add CrUX field data (needs the API key). |
| `compareWithPrevious` | boolean | `true` | Compare with the last stored snapshot and report deltas and regressions. |
| `stateKey` | string | `default` | Monitor name. History is stored in the key-value store `cwv-monitor-<name>`. |
| `thresholds` | object | see below | Regression limits and absolute budgets (`0` = off). |
| `webhookUrl` | string | – | Receives one JSON POST with all alerts of the run (only when there are alerts). |
| `alertOnly` | boolean | `false` | Store only items with alerts in the dataset. All URLs are still audited and charged. |
| `maxConcurrency` | integer | `3` | Parallel audits (1–10). |
| `requestTimeoutSecs` | integer | `120` | Timeout per PSI call (30–300). |
| `maxRetries` | integer | `4` | Retries for 429, 5xx and timeouts (0–8). |
| `maxRetryAfterSecs` | integer | `60` | Longest wait the Actor accepts from a `Retry-After` header (seconds or HTTP date) before retrying (0–600). Longer requested waits are capped; without the header, exponential backoff with jitter is used. |

Threshold defaults used by the code when a value is not set: `maxScoreDrop` 5, `maxLcpIncreasePct` 20, `maxTbtIncreasePct` 30, `maxClsIncrease` 0.05, `maxInpIncreasePct` 20; the budgets `minPerformanceScore`, `lcpBudgetMs`, `clsBudget` and `inpBudgetMs` are off (0). The Console form prefills LCP 2500 ms, CLS 0.1 and INP 200 ms budgets.

### Output example

One dataset item per URL × strategy (example from a local test run against a mocked API):

```json
{
    "url": "https://regress.test/",
    "strategy": "mobile",
    "checkedAt": "2026-10-25T22:53:55.272Z",
    "status": "ok",
    "finalUrl": "https://regress.test/",
    "performanceScore": 61,
    "categoryScores": { "performance": 61 },
    "lab": { "performanceScore": 61, "lcpMs": 4200, "cls": 0.21, "tbtMs": 650, "fcpMs": 900, "speedIndexMs": 1500, "ttfbMs": 120 },
    "field": { "source": "crux-url", "lcpP75Ms": 3900, "inpP75Ms": 320, "clsP75": 0.18, "fcpP75Ms": 1100, "ttfbP75Ms": 350, "overallCategory": "AVERAGE", "collectionPeriod": "2026-09-26..2026-10-23" },
    "cwvAssessment": "AVERAGE",
    "previous": { "checkedAt": "2026-10-25T22:53:46.228Z", "performanceScore": 88, "lab": { "performanceScore": 88, "lcpMs": 2300, "cls": 0.02, "tbtMs": 180, "fcpMs": 900, "speedIndexMs": 1500, "ttfbMs": 120 }, "field": { "source": "crux-url", "lcpP75Ms": 2150, "inpP75Ms": 150, "clsP75": 0.03, "fcpP75Ms": 1100, "ttfbP75Ms": 350, "overallCategory": "FAST", "collectionPeriod": "2026-09-26..2026-10-23" } },
    "deltas": { "performanceScore": -27, "lcpMs": 1900, "tbtMs": 470, "cls": 0.19, "fieldLcpP75Ms": 1750, "fieldInpP75Ms": 170, "fieldClsP75": 0.15 },
    "alert": true,
    "alerts": [
        { "type": "regression", "metric": "performanceScore", "previous": 88, "current": 61, "threshold": 5, "message": "Performance score dropped 88 → 61 (more than 5 points)." },
        { "type": "regression", "metric": "lcpMs", "previous": 2300, "current": 4200, "threshold": 20, "message": "lcpMs worsened 2300 ms → 4200 ms (+83%, limit 20%)." }
    ],
    "lighthouseVersion": "12.8.2",
    "runWarnings": [],
    "cruxError": null,
    "error": null,
    "errorCode": null
}
```

Error item (free):

```json
{ "url": "https://example.com/", "strategy": "mobile", "checkedAt": "2026-10-25T22:54:06.564Z", "status": "error", "errorCode": "QUOTA_DAILY_EXHAUSTED", "error": "PageSpeed Insights API: daily quota exhausted (quota is 0 – an API key is required). Provide your own Google API key or wait for the quota reset." }
```

Item when no API key was provided and Google refused the keyless request (free, the run still succeeds):

```json
{ "url": "https://example.com/", "strategy": "mobile", "checkedAt": "2026-10-26T15:00:00.000Z", "status": "skipped_no_api_key", "errorCode": "API_KEY_REQUIRED", "error": "Skipped (not charged): no Google API key provided and Google allows (almost) no PageSpeed Insights requests without one. Add your own free key in the \"Google API key\" field (Google Cloud Console → APIs & Services → Credentials, enable \"PageSpeed Insights API\"). Google said: PageSpeed Insights API: daily quota exhausted (quota is 0 – an API key is required). Provide your own Google API key or wait for the quota reset." }
```

#### Output fields

| Field | Description |
| --- | --- |
| `url`, `finalUrl`, `strategy`, `checkedAt` | Audited URL, final URL reported by Lighthouse, `mobile`/`desktop`, timestamp. |
| `status` | `ok`, `error`, or `skipped` (budget reached). |
| `performanceScore` | Lighthouse performance score, 0–100. |
| `categoryScores` | Scores (0–100) of `performance` and any extra categories. |
| `lab` | `lcpMs`, `cls`, `tbtMs`, `fcpMs`, `speedIndexMs`, `ttfbMs`, `performanceScore`. |
| `field` | `source` (`crux-url`, `crux-origin`, `psi-url`, `psi-origin`), `lcpP75Ms`, `inpP75Ms`, `clsP75`, `fcpP75Ms`, `ttfbP75Ms`, `overallCategory`, `collectionPeriod`; `null` if no field data. |
| `cwvAssessment` | `FAST` / `AVERAGE` / `SLOW` from the field data, or `null`. |
| `previous`, `deltas` | Previous snapshot and the change per metric (`null` on the first run). |
| `alert`, `alerts` | `true` if any alert fired; list of `{ type, metric, previous, current, threshold, message }`. |
| `lighthouseVersion`, `runWarnings` | Lighthouse version and warnings returned by PSI. |
| `cruxError` | CrUX problem for this item (PSI data is still returned). |
| `error`, `errorCode` | Error details. |

**Error codes:** `PAGE_LOAD_FAILED`, `TIMEOUT`, `RATE_LIMITED`, `SERVER_ERROR`, `API_KEY_INVALID`, `API_ACCESS_DENIED`, `QUOTA_DAILY_EXHAUSTED`, `BAD_REQUEST`, `HTTP_ERROR`, `REQUEST_FAILED`, `INVALID_URL`, `BUDGET_REACHED`.

**Run summary** (`OUTPUT` record): `urls`, `strategies`, `audited`, `alerts`, `itemsWithAlerts`, `webhook` (`sent`, `status` or `error`), `skippedNoApiKey`, `fatalError`.

### Integrations, webhook and API

**Alert webhook:** when a run has alerts, the Actor sends one `POST` request with this JSON body to `webhookUrl`:

```json
{
    "text": ":rotating_light: PageSpeed/Core Web Vitals monitor: 12 alert(s) on 6 URL × strategy check(s)\n• https://regress.test/ (mobile) – Performance score dropped 88 → 61 (more than 5 points).\n…",
    "actor": "pagespeed-cwv-monitor",
    "runId": "<run ID>",
    "checkedAt": "2026-10-25T22:54:00.344Z",
    "alertCount": 12,
    "items": [{ "url": "https://regress.test/", "strategy": "mobile", "alerts": [{ "type": "regression", "metric": "performanceScore", "previous": 88, "current": 61, "threshold": 5, "message": "Performance score dropped 88 → 61 (more than 5 points)." }] }]
}
```

The top-level `text` field is a human-readable summary (one line per alert, at most 30 lines), so a **Slack incoming webhook** URL works directly, as do chat tools that read `text`. The structured fields stay in the same body, so you can also point it at a Make, Zapier or n8n webhook or your own endpoint.

**Apify API:** results are in a standard Apify dataset, so you can use the Apify API, API clients, schedules and integrations.

```bash
curl -X POST "https://api.apify.com/v2/acts/grace_hartwell~pagespeed-cwv-monitor/run-sync-get-dataset-items?token=[YOUR_APIFY_TOKEN]" \
  -H "Content-Type: application/json" \
  -d '{"urls":[{"url":"https://web.dev/"}],"apiKey":"YOUR_GOOGLE_API_KEY","strategies":["mobile"]}'
```

### FAQ

#### Why do I need my own Google API key?

Without a key, the PageSpeed Insights API currently allows (almost) no requests; in testing, a keyless request came back with a daily quota of 0. The CrUX API always requires a key. Your own key gives you your own Google quota. If you run the Actor without a key, it tries the keyless API once per URL × strategy (at most one retry); refused audits are saved as free `skipped_no_api_key` items, nothing is charged, and the run ends successfully with a hint to add a key.

#### What is the difference between lab data and field data?

Lab data comes from a single Lighthouse run in Google's infrastructure, and it can vary from run to run. Field data is the 28-day, 75th-percentile experience of real Chrome users (CrUX). Use the regression thresholds to absorb normal lab variation.

#### Why is there no field data for my URL?

CrUX only has data for pages and origins with enough real-user traffic. The Actor falls back from URL level to origin level; if neither has data, `field` is `null`.

#### How is `cwvAssessment` calculated?

With CrUX data, the Actor calculates it: `FAST` if every available metric is good at p75 (LCP ≤ 2500 ms, INP ≤ 200 ms, CLS ≤ 0.1), `SLOW` if any is poor (LCP > 4000 ms, INP > 500 ms, CLS > 0.25), otherwise `AVERAGE`. Without CrUX, it uses the overall category returned by PageSpeed Insights.

#### Where is the monitoring history stored?

In a key-value store named `cwv-monitor-<stateKey>` in your Apify account. It holds the latest snapshot per URL × strategy, and each run's dataset keeps that run's full results.

#### What happens if my API key is invalid or my quota runs out?

The run stops early with a clear message (`API_KEY_INVALID`, `API_ACCESS_DENIED` or `QUOTA_DAILY_EXHAUSTED`) instead of trying every URL. Failed audits are not charged. (Without any key, see the previous question: audits are skipped for free and the run succeeds.)

#### Is `alertOnly` cheaper?

No. All URLs are still audited and charged, and the history is still updated. `alertOnly` only keeps items without alerts out of the dataset.

### Limitations: what this Actor does not do

- **It does not run Lighthouse itself**: all lab data comes from Google's PageSpeed Insights API, so results depend on Google's availability, quotas and run-to-run variance. Audits can take 10–60 seconds each.
- **No crawling or URL discovery**: it audits only the URLs you list.
- **Scores and metrics only**: it does not return Lighthouse opportunities, diagnostics or the full Lighthouse report.
- **Only the previous snapshot** is used for comparison (one stored snapshot per URL × strategy), not a long-term trend.
- **Long `Retry-After` waits are capped**: the Actor waits as long as Google asks (seconds or HTTP date), but at most `maxRetryAfterSecs` (default 60 s) per retry; daily-quota errors are not retried at all.
- **The webhook is a single JSON POST with a plain `text` summary**: no Slack Block Kit layout, Teams cards or e-mail. It is sent once per run, only when there are alerts, and a failed webhook does not fail the run (see `webhook` in `OUTPUT`).
- **Budget alerts are off by default via API**: the LCP, CLS, INP and score budgets are `0` (off) unless you set them; only the Console form prefills them.
- **Each run is capped** by your *Max total charge*; remaining audits are reported as `skipped` / `BUDGET_REACHED`.

***

Author: Grace M. Hartwell

# Actor input Schema

## `urls` (type: `array`):

Pages to audit. Each URL is audited once per selected strategy.

## `apiKey` (type: `string`):

Your own Google Cloud API key with the 'PageSpeed Insights API' (and optionally 'Chrome UX Report API') enabled. Stored encrypted by Apify; never logged. Free quota: 25,000 PSI requests/day, 150 CrUX requests/minute. Without a key, Google allows (almost) no requests: audits are then skipped (free) with status "skipped_no_api_key".

## `strategies` (type: `array`):

mobile and/or desktop.

## `categories` (type: `array`):

Performance is always included. Add accessibility, best-practices or seo scores if needed.

## `includeCrux` (type: `boolean`):

Query the Chrome UX Report API for p75 LCP, INP, CLS, FCP, TTFB (URL level, falls back to origin). Requires the API key.

## `compareWithPrevious` (type: `boolean`):

Load the last result per URL+strategy from the monitor's key-value store and report deltas + regressions.

## `stateKey` (type: `string`):

History is stored in the key-value store cwv-monitor-<name>. Use different names for different projects.

## `thresholds` (type: `object`):

maxScoreDrop (points), maxLcpIncreasePct, maxTbtIncreasePct, maxClsIncrease, maxInpIncreasePct, and optional absolute budgets minPerformanceScore, lcpBudgetMs, clsBudget, inpBudgetMs (0 = off).

## `webhookUrl` (type: `string`):

Optional: receives one POST with all alerts of the run (JSON) – e.g. Slack/Make/Zapier/n8n webhook. Sent only if there are alerts.

## `alertOnly` (type: `boolean`):

All URLs are still audited (and charged) and the history is updated.

## `maxConcurrency` (type: `integer`):

PSI audits take 10–60 s each. Keep this low to stay within your Google quota.

## `requestTimeoutSecs` (type: `integer`):

Timeout per PageSpeed Insights call.

## `maxRetries` (type: `integer`):

Retries for rate limits (429), server errors and timeouts, with exponential backoff.

## `maxRetryAfterSecs` (type: `integer`):

When Google answers 429/503 with a Retry-After header (seconds or HTTP date), the Actor waits that long – but at most this many seconds – before retrying. Without the header, exponential backoff is used.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://example.com/"
    }
  ],
  "strategies": [
    "mobile",
    "desktop"
  ],
  "categories": [],
  "includeCrux": true,
  "compareWithPrevious": true,
  "stateKey": "default",
  "thresholds": {
    "maxScoreDrop": 5,
    "maxLcpIncreasePct": 20,
    "maxTbtIncreasePct": 30,
    "maxClsIncrease": 0.05,
    "maxInpIncreasePct": 20,
    "minPerformanceScore": 0,
    "lcpBudgetMs": 2500,
    "clsBudget": 0.1,
    "inpBudgetMs": 200
  },
  "alertOnly": false,
  "maxConcurrency": 3,
  "requestTimeoutSecs": 120,
  "maxRetries": 4,
  "maxRetryAfterSecs": 60
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        {
            "url": "https://example.com/"
        }
    ],
    "thresholds": {
        "maxScoreDrop": 5,
        "maxLcpIncreasePct": 20,
        "maxTbtIncreasePct": 30,
        "maxClsIncrease": 0.05,
        "maxInpIncreasePct": 20,
        "minPerformanceScore": 0,
        "lcpBudgetMs": 2500,
        "clsBudget": 0.1,
        "inpBudgetMs": 200
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("grace_hartwell/pagespeed-cwv-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": [{ "url": "https://example.com/" }],
    "thresholds": {
        "maxScoreDrop": 5,
        "maxLcpIncreasePct": 20,
        "maxTbtIncreasePct": 30,
        "maxClsIncrease": 0.05,
        "maxInpIncreasePct": 20,
        "minPerformanceScore": 0,
        "lcpBudgetMs": 2500,
        "clsBudget": 0.1,
        "inpBudgetMs": 200,
    },
}

# Run the Actor and wait for it to finish
run = client.actor("grace_hartwell/pagespeed-cwv-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    {
      "url": "https://example.com/"
    }
  ],
  "thresholds": {
    "maxScoreDrop": 5,
    "maxLcpIncreasePct": 20,
    "maxTbtIncreasePct": 30,
    "maxClsIncrease": 0.05,
    "maxInpIncreasePct": 20,
    "minPerformanceScore": 0,
    "lcpBudgetMs": 2500,
    "clsBudget": 0.1,
    "inpBudgetMs": 200
  }
}' |
apify call grace_hartwell/pagespeed-cwv-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,grace_hartwell/pagespeed-cwv-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/4CuhE1qN4dB2v6azH/builds/vMSNLWnbJfexbNNqK/openapi.json
