# Batch PageSpeed & Core Web Vitals — Mobile+Desktop+CrUX (`ingenious_quip_bxq/batch-pagespeed-cwv`) Actor

Batch Google PageSpeed Insights via the official PSI API: mobile + desktop Lighthouse scores, LCP/CLS/TBT/FCP lab metrics and CrUX field data (LCP/CLS/INP p75). Markdown + CSV report. URL list, sitemap or dataset input. Bring your own free Google API key. Failed URLs free. 256 MB.

- **URL**: https://apify.com/ingenious_quip_bxq/batch-pagespeed-cwv.md
- **Developed by:** [新世紀書僮](https://apify.com/ingenious_quip_bxq) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 pagespeed reports

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

## Batch PageSpeed & Core Web Vitals — PSI API (Mobile + Desktop + CrUX)

Run **Google PageSpeed Insights** for a list of URLs, a sitemap, or another Actor's dataset — **mobile and desktop** in one run — and get:

- **Lighthouse scores** (performance; optionally accessibility, best practices, SEO)
- **Lab Core Web Vitals**: LCP, CLS, TBT, plus FCP, Speed Index, TTI, server response time (TTFB)
- **CrUX field data** (real-user 28-day p75: LCP, CLS, INP, FCP, TTFB + good / needs-improvement / poor) whenever Google returns it, for the URL and for the origin
- Top Lighthouse **opportunities** by estimated savings
- A **Markdown report** (`REPORT.md`) and a **CSV** (`REPORT.csv`) in the run's key-value store, plus one dataset row per URL × strategy

It calls the official **PageSpeed Insights v5 HTTP API** only. No local Chrome, no Lighthouse binary, so the default memory is **256 MB**.

### Bring your own Google API key (important)

Google's PSI API needs an API key for any real volume. **Without a key, Google's shared keyless quota usually answers `HTTP 429 – Quota exceeded … Queries per day`.** When that happens the Actor:

- marks the URL × strategy row as `status: failed`, `errorClass: quota_exceeded`, with a clear hint,
- **does not charge** for it,
- stops making further calls for the rest of the run (`stopOnDailyQuota`, on by default), so you are not billed for a run that can't succeed.

Getting a key is free and takes a couple of minutes:

1. Open Google Cloud Console → **APIs & Services → Library** → enable **PageSpeed Insights API**.
2. **APIs & Services → Credentials → Create credentials → API key**. Optionally restrict the key to the PageSpeed Insights API.
3. Paste it into the **`googlePagespeedApiKey`** input field. It is stored as an encrypted secret input, sent to Google in a request header (never in the URL), and never written to logs or output.

This Actor has no wallet, top-up or identity-verification step. The only things you supply are URLs and, optionally, your own Google key.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `urls` | array | — | `{ "url": "…" }` objects, plain strings, or `requestsFromUrl` remote lists |
| `sitemapUrl` | string | — | sitemap.xml / sitemap index (.gz ok) / text list; capped by `maxUrls` |
| `datasetId` | string | — | Dataset from **Sitemap URL Discovery** or **URL Status Checker** (reads `url`; skips rows with `httpStatus >= 400`) |
| `keyValueStoreId` / `keyValueRecordKey` | string | `DOC_TO_MARKDOWN_INPUT` | Load URLs from a KV record |
| `googlePagespeedApiKey` | secret string | — | **Recommended.** Your Google API key |
| `strategies` | array | `["mobile","desktop"]` | Each strategy = one PSI call and one report per URL |
| `categories` | array | `["performance"]` | Add `accessibility`, `best-practices`, `seo` at the same price |
| `locale` | string | — | e.g. `en`, `de`, `ja` |
| `maxUrls` | integer | 100 | 0 = no limit |
| `maxConcurrency` | integer | 2 | 1–5; PSI calls take 10–30 s each |
| `maxRetries` | integer | 3 | Retries per-minute 429, 5xx and timeouts with backoff |
| `requestTimeoutSecs` | integer | 120 | Per PSI call |
| `stopOnDailyQuota` | boolean | true | Skip remaining calls (free) once Google reports the daily quota is exhausted |
| `topOpportunities` | integer | 5 | Opportunities kept per row |
| `includeFailedInCsv` | boolean | true | Include failed rows in `REPORT.csv` |

Example:

```json
{
  "urls": [{ "url": "https://example.com/" }],
  "strategies": ["mobile", "desktop"],
  "googlePagespeedApiKey": "YOUR_KEY"
}
```

### Output

**Dataset** (one row per URL × strategy), main fields:

`url, strategy, status, finalUrl, performanceScore, accessibilityScore, bestPracticesScore, seoScore, labLcpMs, labCls, labTbtMs, labFcpMs, labSpeedIndexMs, labTtiMs, labTtfbMs, cruxAvailable, cruxIsOriginFallback, cruxOverallCategory, cruxLcpP75, cruxLcpCategory, cruxClsP75, cruxClsCategory, cruxInpP75, cruxInpCategory, cruxFcpP75, cruxTtfbP75, cruxOrigin…, topOpportunities, lighthouseVersion, fetchTime, httpStatus, attempts, durationMs, errorClass, errorMessage`

**Key-value store**: `REPORT.md` (lab table, CrUX table, opportunities, failed rows), `REPORT.csv`, `SUMMARY` / `OUTPUT` (counts, error classes, whether a key was used, what was charged).

`errorClass` values: `quota_exceeded`, `skipped_quota`, `api_key_invalid`, `api_forbidden`, `lighthouse_error`, `bad_request`, `psi_server_error`, `timeout`, `network`.

CrUX notes: CLS p75 is converted to the usual unitless value (Google's API returns it ×100). Low-traffic pages often have no URL-level field data. In that case PSI may fall back to origin data (`cruxIsOriginFallback: true`), or there may be none at all.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| Actor start | $0.001 | once per run (Apify synthetic event) |
| `pagespeed-report` | $0.003 | per **successful** URL × strategy report |

One URL with mobile + desktop is 2 reports, $0.006. **Failed calls are free**, including 429 quota, invalid key, timeouts and Lighthouse page-load errors. Your Google API usage is billed by Google under your own key's terms; PSI is normally free within Google's quota.

### Chaining

- **Sitemap URL Discovery** → pass its dataset id as `datasetId` to audit a whole site section.
- **URL Status Checker** → pass its dataset id; broken URLs (`httpStatus >= 400`) are skipped automatically.

### Limits & notes

- Lab metrics vary from run to run (network, Google's test location). Compare trends, not single runs.
- PSI can't test pages behind login, on localhost, or ones that block Google's Lighthouse user agent. Those come back as `lighthouse_error`, free.
- Results are computed by Google's servers and returned as-is. INP exists only as field data (CrUX); lab responsiveness shows up as TBT.

### License

AGPL-3.0. See `LICENSE`.

# Changelog

This Actor's version history is a separate document: https://apify.com/ingenious_quip_bxq/batch-pagespeed-cwv/changelog.md

# Actor input Schema

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

Pages to test. Accepts {"url": "..."} objects (same shape as Sitemap / URL-status dataset rows), plain strings, and remote lists (requestsFromUrl).

## `sitemapUrl` (type: `string`):

Optional. sitemap.xml (urlset or sitemap index, .gz ok) or a plain-text URL list. Capped by Max URLs. For very large sites run Sitemap URL Discovery first and pass its dataset id.

## `datasetId` (type: `string`):

Optional. Apify dataset ID from Sitemap URL Discovery or URL Status Checker. Reads the `url` field; rows with httpStatus >= 400 are skipped.

## `keyValueStoreId` (type: `string`):

Optional. Load URLs from a KV record such as DOC_TO_MARKDOWN_INPUT.

## `keyValueRecordKey` (type: `string`):

Key inside the source key-value store. Ignored unless a store ID is set.

## `googlePagespeedApiKey` (type: `string`):

Your own Google API key with the 'PageSpeed Insights API' enabled (created for free in Google Cloud Console → APIs & Services → Credentials → Create API key, then enable PageSpeed Insights API). WITHOUT a key Google's shared keyless quota usually returns HTTP 429 'Quota exceeded' — those URL × strategy rows are marked failed and are NOT charged. Stored encrypted; never logged.

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

Lighthouse device emulation. Each selected strategy is one PSI call and one billable report per URL.

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

Performance is always included. Adding Accessibility / Best practices / SEO adds those scores (same price, slightly slower calls).

## `locale` (type: `string`):

Optional locale for audit titles (e.g. en, de, ja). Empty = Google default (en).

## `maxUrls` (type: `integer`):

Stop after this many unique URLs (0 = no limit). Total reports = URLs × strategies.

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

Parallel PSI calls. PSI takes 10–30 s per call and Google rate-limits per key per minute, so 2–3 is usually safe.

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

Retries for per-minute HTTP 429, 5xx and timeouts (exponential backoff). A per-day quota 429 is not retried.

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

Timeout for each PSI API call.

## `stopOnDailyQuota` (type: `boolean`):

When Google reports the DAILY quota is exhausted, skip the remaining calls (free) instead of hammering the API.

## `topOpportunities` (type: `integer`):

How many Lighthouse opportunities (sorted by estimated savings) to keep per row. 0 = none.

## `includeFailedInCsv` (type: `boolean`):

REPORT.csv also lists failed URL × strategy rows with errorClass / errorMessage.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://example.com/"
    }
  ],
  "keyValueRecordKey": "DOC_TO_MARKDOWN_INPUT",
  "strategies": [
    "mobile",
    "desktop"
  ],
  "categories": [
    "performance"
  ],
  "maxUrls": 10,
  "maxConcurrency": 2,
  "maxRetries": 3,
  "requestTimeoutSecs": 120,
  "stopOnDailyQuota": true,
  "topOpportunities": 5,
  "includeFailedInCsv": true
}
```

# Actor output Schema

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

Dataset items: url, strategy, status, performanceScore, lab LCP/CLS/TBT/FCP/SI/TTI, CrUX p75 LCP/CLS/INP, errorClass.

## `reportMarkdown` (type: `string`):

Human-readable summary tables: lab results, CrUX field data, top opportunities, failed rows.

## `reportCsv` (type: `string`):

Flat CSV, one line per URL × strategy (failed rows included unless includeFailedInCsv=false).

## `summary` (type: `string`):

reportsOk, reportsFailed, byClass, apiKeyProvided, dailyQuotaExhausted, charged, durationSecs, peakMemoryMb.

# 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/"
        }
    ],
    "strategies": [
        "mobile",
        "desktop"
    ],
    "maxUrls": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("ingenious_quip_bxq/batch-pagespeed-cwv").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/" }],
    "strategies": [
        "mobile",
        "desktop",
    ],
    "maxUrls": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("ingenious_quip_bxq/batch-pagespeed-cwv").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/"
    }
  ],
  "strategies": [
    "mobile",
    "desktop"
  ],
  "maxUrls": 10
}' |
apify call ingenious_quip_bxq/batch-pagespeed-cwv --silent --output-dataset

```

## MCP server setup

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

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/IAjK7BWzUIdRqkJLK/builds/PI3Oaj9mWfe95s4pM/openapi.json
