# Core Web Vitals Alerts: Score Drops Since Last Run (`accountable_eel/core-web-vitals-regression-alerts`) Actor

Check a list of page URLs with Google PageSpeed Insights on your own free API key and get the score, LCP, CLS and INP, plus an alert mode that only returns and bills the URLs whose performance regressed since your last run.

- **URL**: https://apify.com/accountable_eel/core-web-vitals-regression-alerts.md
- **Developed by:** [Adriana Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 url checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Core Web Vitals Alerts: Score Drops Since Last Run

**Find out which of your pages got slower since the last check, before Google or your customers do.**
Give it a list of page URLs and your own free Google API key. For each URL it runs Google PageSpeed Insights (Lighthouse) and returns the performance score plus LCP, CLS and TBT, with INP, LCP and CLS from real Chrome users where Google has enough traffic data. Turn on monitoring and run it on a schedule: it remembers the last numbers per URL and only returns, and only bills, the URLs whose score or Core Web Vitals got worse.

**Who uses it:** SEO and web teams watching key landing pages after every release, agencies keeping an eye on client sites, and e-commerce owners who want a ping when a theme or app update slows the shop down.

**You bring the key.** This actor never contains, stores or shares a Google key. You paste your own into the secret "Your Google API key" field; it is used only for your run. A free key takes about 10 minutes to create (Google Cloud Console, APIs and Services, Credentials, then enable the PageSpeed Insights API) and covers up to 25,000 checks a day. **Without a key the actor still runs**: every URL comes back as a free `NEEDS_KEY` row that tells you where to get one.

**Related actors:** for a one-off technology check of a site use [Tech Stack Lookup](https://apify.com/accountable_eel/tech-stack-lookup).

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `urls` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~core-web-vitals-regression-alerts/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"urls":["https://example.com"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

### Input

```json
{
  "urls": [
    "https://example.com"
  ]
}
```

One page URL per line (https:// is added if missing). Each URL is one Google PageSpeed Insights run, so a long list takes a few minutes. Needs your own free Google API key below. Accepted formats: https://example.com, www.example.com/pricing, https://shop.example.com/products/best-seller.

**Alert thresholds.** Mobile scores jitter by a few points between identical runs, so a change only counts once it clears a tolerance band you control: the performance score must drop by "Alert when the score drops by" points (default 5), and LCP, CLS or INP must move into a worse Google band (LCP 2500/4000 ms, CLS 0.1/0.25, INP 200/500 ms) and get at least "Alert when LCP, CLS or INP worsen by" percent worse (default 20). INP comes from real-user data, so it is only compared when Google has it for the URL on both runs.

**Monitoring.** With "Only return URLs that regressed since the last run" on, the first run stores a baseline for each URL (returned and billed once). Later runs return and bill only URLs that regressed; URLs that held steady or improved come back as free `STABLE` / `IMPROVED` rows with `urlCount` 0. The memory lives in a named key-value store of this actor on your account, one record per URL and device. A URL that fails to measure never overwrites its baseline.

**One check takes 10 to 30 seconds** (Google runs a real Lighthouse test), so 50 URLs take a few minutes. A page Google cannot load is retried by the platform, which can cost a few of your own quota units.

### Output

One row per URL. Numbers Google does not have are `null`, never 0 (a page with a real CLS of 0 shows 0). Real-user fields (`inpMs`, `fieldLcpMs`, `fieldCls`, `fieldCategory`) are `null` for pages without enough Chrome traffic. Every row carries the page address and a `reportUrl` to Google's own report.

| query | found | status | pageUrl | strategy | performanceScore | lcpMs | cls | tbtMs | inpMs | fieldLcpMs | fieldCls | fieldCategory | monitorStatus | regressed | regressions | previousScore | previousLcpMs | previousCls | previousInpMs | previousCheckedAt | reportUrl | urlCount | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| https://example.com | true | OK | <page tested> | <device> | \<performance score (0-100)> | \<lcp, lab (ms)> | \<cls, lab> | \<tbt, lab (ms)> | \<inp, real users (ms)> | \<lcp, real users (ms)> | \<cls, real users> | <real-user verdict> | <monitoring status> | <regressed since last run> | <what got worse> | <previous score> | \<previous lcp, lab (ms)> | \<previous cls, lab> | \<previous inp (ms)> | <previous check> | <full pagespeed report> | <billed checks> | 1970-01-01T00:00:00.000Z |

A URL that could not be measured comes back as a row with `"found": false` and a `status`: `NEEDS_KEY` (no key, a malformed key, a rejected key, or the PageSpeed Insights API not enabled for the key's project), `BAD_FORMAT`, `NOT_FOUND` (Lighthouse could not load the page), `BLOCKED` (your Google quota is used up) or `REQUEST_FAILED`. Those rows are never charged.

### Pricing

Pay-per-event. A flat per-run fee covers session/proxy warmup; you're billed
per item only when data is actually found and returned — see
`.actor/pay_per_event.json` for exact prices. A miss is never charged.

**What is billed.** One `url-checked` event per URL measured. A URL that could not be measured, a `NEEDS_KEY` row and, in monitoring mode, a quiet `STABLE` or `IMPROVED` row are never charged. The Google API call itself runs on your own key and your own Google quota (25,000 checks a day, 240 a minute); Google does not charge for it.

### Example runs

These three runs use no key, so they are free and show exactly what a first test returns.

**1. One URL, no key.**

```json
{ "urls": ["https://example.com"] }
```

```json
{ "query": "https://example.com", "found": false, "status": "NEEDS_KEY", "message": "No Google API key was supplied, so nothing was measured for this URL. Create a free key in Google Cloud Console (APIs and Services, Credentials), enable the PageSpeed Insights API, then paste it into the \"Your Google API key\" field. It takes about 10 minutes and is free up to 25,000 checks a day.", "scrapedAt": "2026-10-11T22:44:39.476Z" }
```

**2. A list with a bare domain and a typo.** `www.example.org/pricing` gets the same `NEEDS_KEY` row as above (https:// is added for you); `not a url` gets `"status": "BAD_FORMAT"` with the message "This does not look like a page URL. Use a full address such as https://example.com/pricing." Both are free.

```json
{ "urls": ["https://example.com", "www.example.org/pricing", "not a url"] }
```

**3. Monitoring switched on, still no key.** Nothing is measured and nothing is remembered, so your later keyed run is still a clean first baseline.

```json
{ "urls": ["https://example.com"], "onlyNewSinceLastRun": true, "deltaName": "landing-pages" }
```

### With your key

One URL with a key and monitoring on, a real run on `https://example.com` (mobile). The first monitoring run stores the baseline, so the `previous*` fields are `null`:

```json
{ "urls": ["https://example.com"], "googleApiKey": "<your key>", "onlyNewSinceLastRun": true }
```

```json
{
  "query": "https://example.com", "found": true, "status": "OK", "scrapedAt": "2026-10-11T22:45:19.267Z",
  "pageUrl": "https://example.com/", "strategy": "mobile",
  "performanceScore": 100, "lcpMs": 754, "cls": 0, "tbtMs": 0,
  "inpMs": 87, "fieldLcpMs": 1049, "fieldCls": 0, "fieldCategory": "FAST",
  "monitorStatus": "BASELINE", "regressed": null, "regressions": null,
  "previousScore": null, "previousLcpMs": null, "previousCls": null, "previousInpMs": null, "previousCheckedAt": null,
  "reportUrl": "https://pagespeed.web.dev/analysis?url=https%3A%2F%2Fexample.com", "urlCount": 1
}
```

On a later run the same URL comes back like this only if it got worse, for example `"monitorStatus": "REGRESSED"`, `"regressions": ["performance score 92 to 78", "LCP 2000 ms to 3100 ms"]`, with the earlier numbers in the `previous*` fields.

### FAQ

**Why do I need my own key?** Google gives the shared, keyless project no quota for this API, and a key of your own keeps your usage, your quota and your Google account separate from everyone else's. The actor never reads, stores or returns the key beyond your own run input.

**Mobile or desktop?** Mobile by default, which is the profile Google ranks on. Desktop and mobile keep separate monitoring memories.

**Why did my score change when nothing changed?** Lab scores are a simulation and move a few points run to run. That is why alerts need a drop of 5 points by default; raise it for noisy pages.

**Why is INP empty?** INP is a real-user number. Google only publishes it for URLs with enough Chrome traffic.

**Does it check pages behind a login?** No. Google must be able to open the URL without signing in.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~core-web-vitals-regression-alerts/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://example.com"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~core-web-vitals-regression-alerts/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"urls":["https://example.com"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~core-web-vitals-regression-alerts/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"urls":["{{URL}}"]}`, mapping the row's URL into the `urls` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "core web vitals monitor" — the agent will find and run this actor.

# Actor input Schema

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

One page URL per line (https:// is added if missing). Each URL is one Google PageSpeed Insights run, so a long list takes a few minutes. Needs your own free Google API key below. Accepted formats: https://example.com, www.example.com/pricing, https://shop.example.com/products/best-seller. You're only charged for the ones we actually find — a miss costs nothing.

## `googleApiKey` (type: `string`):

Optional but needed for real results. Create a free key in Google Cloud Console (APIs and Services, Credentials), enable the PageSpeed Insights API for the project, about 10 minutes, free up to 25,000 checks a day. This actor never ships or shares a key; yours is used only for this run. Without a key every row comes back as NEEDS_KEY, free, so you can see the shape.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `strategy` (type: `string`):

Which Lighthouse profile to test. Mobile is what Google ranks on. Switching device starts a separate monitoring memory.

## `scoreDropPoints` (type: `integer`):

Tolerance band for the 0-100 performance score. Lab scores jitter by a few points between identical runs, so a drop smaller than this is STABLE. Default 5.

## `metricWorsenPercent` (type: `integer`):

A metric counts as regressed only when it moved into a worse Google band (LCP 2500/4000 ms, CLS 0.1/0.25, INP 200/500 ms) AND got at least this much worse than last run. Default 20.

## `onlyNewSinceLastRun` (type: `boolean`):

On: the first run stores a baseline (returned and billed once per URL); after that only URLs that crossed a threshold are returned and billed, unchanged URLs come back as free STABLE rows. Run it on a schedule as an alert. Off: every URL is measured and billed every time.

## `deltaName` (type: `string`):

Optional. Name this monitoring memory so two schedules of this actor stay apart, or keep one memory across a device change. Leave empty to derive one from the device.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

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

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

## Actor input object example

```json
{
  "urls": [
    "https://example.com"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "strategy": "mobile",
  "scoreDropPoints": 5,
  "metricWorsenPercent": 20,
  "onlyNewSinceLastRun": false,
  "deltaName": "",
  "columns": [
    "pageUrl",
    "strategy",
    "performanceScore",
    "lcpMs",
    "cls",
    "tbtMs",
    "inpMs",
    "fieldLcpMs",
    "fieldCls",
    "fieldCategory",
    "monitorStatus",
    "regressed",
    "regressions",
    "previousScore",
    "previousLcpMs",
    "previousCls",
    "previousInpMs",
    "previousCheckedAt",
    "reportUrl",
    "urlCount"
  ],
  "maxConcurrency": 2
}
```

# 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": [
        "https://example.com"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/core-web-vitals-regression-alerts").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": ["https://example.com"],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/core-web-vitals-regression-alerts").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": [
    "https://example.com"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/core-web-vitals-regression-alerts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/core-web-vitals-regression-alerts"
        }
    }
}
```

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/85PSq4jKDkYZ7cPEO/builds/srGnfUsUg5eA2Lpiz/openapi.json
