---
name: "Google SEO APIs"
repo: https://github.com/agricidaniel/claude-seo
source: https://github.com/agricidaniel/claude-seo/blob/HEAD/skills/seo-google/SKILL.md
license: "MIT"
install: "npx skills add agricidaniel/claude-seo --skill seo-google"
page: https://100xlab.com/resources/agent-skills/agricidaniel-claude-seo--seo-google--144bix9
---

# Google SEO APIs

Direct access to Google's own SEO data. Bridges the gap between crawl-based
analysis (existing claude-seo skills) and Google's real-time field data: actual
Chrome user metrics, real indexation status, search performance, and organic traffic.

All APIs are free. Setup requires a Google Cloud project with API key and/or
service account -- run `/seo google setup` for step-by-step instructions.

## Prerequisites

Before executing any command, check credentials:
```bash
claude-seo run google_auth.py --check --json
```

Config file: `~/.config/claude-seo/google-api.json`
```json
{
  "service_account_path": "/path/to/service_account.json",
  "api_key": "<GOOGLE_API_KEY>",
  "default_property": "sc-domain:example.com",
  "ga4_property_id": "properties/123456789"
}
```

If missing, read `references/auth-setup.md` and walk the user through setup.

### Credential Tiers

| Tier | Detection | Available Commands |
|------|-----------|-------------------|
| **0** (API Key) | `api_key` present | `pagespeed`, `crux`, `crux-history`, `youtube`, `nlp` |
| **1** (OAuth/SA) | + OAuth token or service account | Tier 0 + `gsc`, `inspect`, `sitemaps`, `index` |
| **2** (Full) | + `ga4_property_id` configured | Tier 1 + `ga4`, `ga4-pages` |
| **3** (Ads) | + `ads_developer_token` + `ads_customer_id` | Tier 2 + `keywords`, `volume` |

Always communicate the detected tier before running commands.

## Quick Reference

| Command | What it does | Tier |
|---------|-------------|------|
| `/seo google setup` | Check/configure API credentials | -- |
| `/seo google pagespeed <url>` | PSI Lighthouse + CrUX field data | 0 |
| `/seo google crux <url>` | CrUX field data only (p75 metrics) | 0 |
| `/seo google crux-history <url>` | 25-week CWV trend analysis | 0 |
| `/seo google gsc <property>` | Search Console: clicks, impressions, CTR, position | 1 |
| `/seo google inspect <url>` | URL Inspection: index status, canonical, crawl info | 1 |
| `/seo google inspect-batch <file>` | Batch URL Inspection from file | 1 |
| `/seo google sitemaps <property>` | GSC sitemap status | 1 |
| `/seo google index <url>` | Submit URL to Indexing API | 1 |
| `/seo google index-batch <file>` | Batch submit up to 200 URLs | 1 |
| `/seo google ga4 [property-id]` | GA4 organic traffic report | 2 |
| `/seo google ga4-pages [property-id]` | Top organic landing pages | 2 |
| `/seo google youtube <query>` | YouTube video search (views, likes, duration) | 0 |
| `/seo google youtube-video <id>` | YouTube video details + top comments | 0 |
| `/seo google nlp <url-or-text>` | NLP entity extraction + sentiment + classification | 0 |
| `/seo google entities <url-or-text>` | Entity analysis only (for E-E-A-T) | 0 |
| `/seo google keywords <seed>` | Keyword ideas from Google Ads Keyword Planner | 3 |
| `/seo google volume <keywords>` | Search volume lookup from Keyword Planner | 3 |
| `/seo google entity <query>` | Knowledge Graph entity check | 0 |
| `/seo google safety <url>` | Web Risk URL safety check | 0 |
| `/seo google quotas` | Show rate limits for all APIs | -- |

---

## PageSpeed + CrUX

### `/seo google pagespeed <url>`

Combined Lighthouse lab data + CrUX field data.

**Script:** `claude-seo run pagespeed_check.py <url> --json`
**Reference:** `references/pagespeed-crux-api.md`
**Default:** Both mobile + desktop strategies, all Lighthouse categories.

Output merges lab scores (point-in-time Lighthouse) with field data (28-day
Chrome user metrics). CrUX tries URL-level first, falls back to origin-level.

### `/seo google crux <url>`

CrUX field data only (no Lighthouse run). Faster.

**Script:** `claude-seo run pagespeed_check.py <url> --crux-only --json`

### `/seo google crux-history <url>`

25-week CrUX History trends. Shows whether CWV metrics are improving, stable, or degrading.

**Script:** `claude-seo run crux_history.py <url> --json`
**Reference:** `references/pagespeed-crux-api.md`

Output includes per-metric trend direction, percentage change, and weekly p75 values.

---

## Search Console

### `/seo google gsc <property>`

Search Analytics: clicks, impressions, CTR, position for last 28 days.

**Script:** `claude-seo run gsc_query.py --property <property> --json`
**Reference:** `references/search-console-api.md`
**Default:** 28 days, dimensions=query,page, type=web, limit=1000.

Includes quick-win detection: queries at position 4-10 with high impressions.
The `totals` block comes from a separate dimensionless aggregate query because
query-level rows can omit anonymized low-volume traffic. Treat totals as
site-wide only when `totals_complete` is true. `--limit` caps total returned
dimension rows, not the size of every pagination request.

> **AI surfaces in GSC (2026):**
> - **Generative AI performance report** (launched 2026-06-03), a dedicated view of **AI Overviews + AI Mode** visibility. **Impressions only** (no clicks/CTR/position/query); dimensions Pages/Countries/Devices/Dates (Pacific Time); 1,000-row limit; newest data preliminary; a separate Discover gen-AI report also exists. Rolling out to a subset of properties.
> - **AI Mode already rolls into standard Performance totals** (Web search type), clicks (external-link clicks in AI Mode) and impressions are counted in the normal report, so you **cannot** cleanly split "classic" vs "AI" traffic from totals. Use the Generative AI report for impressions-only AI visibility.
> - **Data-reliability caveat:** a GSC logging error made **impressions, CTR, and average position unreliable from 2025-05-13 to 2026-04-27** (clicks unaffected; fixed forward-only, **no backfill**). Treat impression/CTR/position trends spanning that window with caution; expect an apparent impressions drop after the fix.

### `/seo google inspect <url>`

URL Inspection: real indexation status from Google.

**Script:** `claude-seo run gsc_inspect.py <url> --json`

Returns: verdict (PASS/FAIL), coverage state, robots.txt status, indexing state,
page fetch state, canonical selection, mobile usability, rich results.

### `/seo google inspect-batch <file>`

Batch inspection from a file (one URL per line). Rate limited to 2,000/day per site.

**Script:** `claude-seo run gsc_inspect.py --batch <file> --json`

### `/seo google sitemaps <property>`

List submitted sitemaps with status, errors, warnings. Sitemap contents report
submitted counts only; URL Inspection API is the indexation truth for whether
specific URLs are indexed.

**Script:** `claude-seo run gsc_query.py sitemaps --property <property> --json`

---

## Indexing API

### `/seo google index <url>`

Notify Google of a URL update.

**Script:** `claude-seo run indexing_notify.py <url> --json`
**Reference:** `references/indexing-api.md`

The Indexing API is officially for JobPosting and BroadcastEvent/VideoObject pages.
Always inform the user of this restriction. Daily quota: 200 publish requests.

### `/seo google index-batch <file>`

Batch submit URLs from a file. Tracks quota usage.

**Script:** `claude-seo run indexing_notify.py --batch <file> --json`

---

## GA4 Traffic

### `/seo google ga4 [property-id]`

Organic traffic report: daily sessions, users, pageviews, bounce rate, engagement.

**Script:** `claude-seo run ga4_report.py --property <id> --json`
**Reference:** `references/ga4-data-api.md`
**Default:** 28 days, filtered to Organic Search channel group.

> **GA4 "AI Assistants" channel (live ~2026-05-13):** GA4 added a native *AI Assistants* Default Channel Group. Sessions referred by a recognized AI assistant get `medium=ai-assistant`. Google's recognized sources are **ChatGPT, Gemini, Claude, Deepseek, Copilot, Grok** and the channel **excludes** Google AI Overviews / AI Mode. **Verify Perplexity separately if needed**; unsupported sources may stay in Referral, and most AI sessions arrive referrer-less and fall into **Direct**, so this channel undercounts AI traffic. Forward-only, no backfill.

### `/seo google ga4-pages [property-id]`

Top organic landing pages ranked by sessions.

**Script:** `claude-seo run ga4_report.py --property <id> --report top-pages --json`

---

## YouTube (Video SEO)

Some third-party studies report a 0.737 correlation between YouTube mentions and AI visibility. Treat it as a methodology-dependent signal. Free, API key only.

### `/seo google youtube <query>`

Search YouTube for videos. Returns title, channel, views, likes, duration.

**Script:** `claude-seo run youtube_search.py search "<query>" --json`
**Reference:** `references/youtube-api.md`
**Quota:** 100 units per search (10,000 units/day free).

### `/seo google youtube-video <video_id>`

Detailed video info + tags + top 10 comments.

**Script:** `claude-seo run youtube_search.py video <video_id> --json`
**Quota:** 2 units (video details + comments).

---

## NLP Content Analysis

Google NLP entity/sentiment output for internal content-quality checks. Do not treat it as Google E-E-A-T scoring.

### `/seo google nlp <url-or-text>`

Full NLP analysis: entities, sentiment, content classification.

**Script:** `claude-seo run nlp_analyze.py --url <url> --json` or `--text "..."`
**Reference:** `references/nlp-api.md`
**Free tier:** 5,000 units/month. Requires billing enabled on GCP project.

### `/seo google entities <url-or-text>`

Entity extraction only (faster, less quota).

**Script:** `claude-seo run nlp_analyze.py --url <url> --features entities --json`

---

## Keyword Research (Google Ads)

Gold-standard keyword volume data. Requires Google Ads account.

### `/seo google keywords <seed>`

Generate keyword ideas from seed terms.

**Script:** `claude-seo run keyword_planner.py ideas "<seed>" --json`
**Reference:** `references/keyword-planner-api.md`
**Requires:** Ads developer token + customer ID in config (Tier 3).

### `/seo google volume <keywords>`

Search volume for specific keywords (comma-separated).

**Script:** `claude-seo run keyword_planner.py volume "<kw1>,<kw2>" --json`

---

## Supplementary

### `/seo google entity <query>`

Knowledge Graph entity check. Verifies brand presence.

**Reference:** `references/supplementary-apis.md`
Uses Knowledge Graph Search API with API key.

### `/seo google safety <url>`

Web Risk API check for malware/social engineering flags.

**Reference:** `references/supplementary-apis.md`

### `/seo google quotas`

Display rate limits table. Read `references/rate-limits-quotas.md`.

---

## Reports

After any analysis command, offer to generate a PDF/HTML report.

### `/seo google report <type>`

Generate a professional PDF report with charts and analytics.

**Script:** `claude-seo run google_report.py --type <type> --data <json> --domain <domain> --format pdf`

| Type | Input | Output |
|------|-------|--------|
| `cwv-audit` | PSI + CrUX + CrUX History data | Core Web Vitals audit with gauges, timelines, distributions |
| `gsc-performance` | GSC query data | Search Console report with query tables, quick wins |
| `indexation` | Batch inspection data | Indexation status with coverage donut chart |
| `full` | All data combined | Comprehensive Google SEO report (all sections) |

**Workflow:**
1. Run data collection commands (pagespeed, gsc, inspect-batch, etc.)
2. Save JSON output to file: `claude-seo run pagespeed_check.py <url> --json > data.json`
3. Generate report: `claude-seo run google_report.py --type cwv-audit --data data.json --domain <domain>`

**Convention:** After completing analysis, suggest: "Generate a report? Use `/seo google report <type>`"

---

## Rate Limits

| API | Per-Minute | Per-Day | Auth |
|-----|-----------|---------|------|
| PSI v5 | 240 QPM | 25,000 QPD | API Key |
| CrUX + History | 150 QPM (shared) | Unlimited | API Key |
| GSC Search Analytics | 1,200 QPM/site | 30M QPD | Service Account |
| GSC URL Inspection | 600 QPM | 2,000 QPD/site | Service Account |
| Indexing API | 380 RPM | 200 publish/day | Service Account |
| GA4 Data API | 10 concurrent | ~25K tokens/day | Service Account |

## Cross-Skill Integration

- **seo-audit**: Spawns `seo-google` agent for live CWV + indexation data (conditional)
- **seo-technical**: Uses pagespeed_check.py for real CWV field data
- **seo-performance**: CrUX field data supplements Lighthouse lab data
- **seo-sitemap**: GSC sitemap status shows submitted counts, errors, and warnings; use URL Inspection for indexation truth
- **seo-content**: GSC query data informs keyword targeting
- **seo-geo**: Use GSC Generative AI performance reports and AI Overviews/AI Mode/Discover gen-AI include/exclude controls where available

## Output Format

- CWV metrics: traffic-light rating (Good / Needs Improvement / Poor)
- Performance reports: tables with sortable columns
- Always include data freshness note
- Save reports as `GOOGLE-API-REPORT-{domain}.md`
- Markdown/LLM templates in `assets/templates/`: `cwv-audit-report.md`, `gsc-performance-report.md`, `indexation-status-report.md`; distinct from `google_report.py`'s PDF pipeline

## Technical Notes

- INP replaced FID on March 12, 2024. Never reference FID.
- CLS values from CrUX are string-encoded (e.g., "0.05"). Scripts handle parsing.
- CrUX 404 = insufficient traffic, not an auth error.
- Search Analytics data has 2-3 day lag.
- `round_trip_time` replaced `effectiveConnectionType` in CrUX (Feb 2025).
- Custom Search JSON API is closed to new customers (2025).

## Error Handling

| Scenario | Action |
|----------|--------|
| No credentials configured | Run `/seo google setup`. List Tier 0 commands that work with just an API key. |
| Service account lacks GSC access | Report error. Instruct: add `client_email` to GSC > Settings > Users > Add. |
| CrUX data unavailable (404) | Report insufficient Chrome traffic. Suggest PSI lab data as fallback. |
| GA4 property not found | Report error. Show how to find property ID in GA4 Admin > Property Details. |
| Indexing API quota exceeded | Report 200/day limit. Suggest prioritizing most important URLs. |
| Rate limit (429) | Wait and retry with exponential backoff. Report which API hit the limit. |

---

## Supporting file: assets/templates/cwv-audit-report.md

# Core Web Vitals Audit

**URL/Origin:** {target}
**Strategy:** {strategy}

## CrUX Field Data (28-day rolling average)

Real Chrome user experience data from the Chrome UX Report.

| Metric | p75 Value | Rating | Good Threshold | Distribution |
|--------|-----------|--------|----------------|-------------|
| LCP | {lcp_value} | {lcp_rating} | ≤ 2,500ms | Good: {lcp_good}% / NI: {lcp_ni}% / Poor: {lcp_poor}% |
| INP | {inp_value} | {inp_rating} | ≤ 200ms | Good: {inp_good}% / NI: {inp_ni}% / Poor: {inp_poor}% |
| CLS | {cls_value} | {cls_rating} | ≤ 0.1 | Good: {cls_good}% / NI: {cls_ni}% / Poor: {cls_poor}% |
| FCP | {fcp_value} | {fcp_rating} | ≤ 1,800ms | Good: {fcp_good}% / NI: {fcp_ni}% / Poor: {fcp_poor}% |
| TTFB | {ttfb_value} | {ttfb_rating} | ≤ 800ms | Good: {ttfb_good}% / NI: {ttfb_ni}% / Poor: {ttfb_poor}% |

**Collection Period:** {collection_start} to {collection_end}

## Lighthouse Lab Scores

| Category | Score |
|----------|-------|
| Performance | {perf_score}/100 |
| Accessibility | {a11y_score}/100 |
| Best Practices | {bp_score}/100 |
| SEO | {seo_score}/100 |

## CrUX History Trends (25-week)

| Metric | Direction | Change | Earliest → Latest |
|--------|-----------|--------|-------------------|
{trends_table}

## Top Opportunities

| Opportunity | Estimated Savings |
|-------------|-------------------|
{opportunities_table}

## Recommendations

{recommendations}

---
*CrUX data updated daily ~04:00 UTC. 28-day rolling average.*
*INP replaced FID as the responsiveness Core Web Vital on March 12, 2024.*
*Generated {timestamp}.*

---

## Supporting file: assets/templates/gsc-performance-report.md

# Google Search Console Performance Report

**Property:** {property}
**Date Range:** {start_date} — {end_date}
**Search Type:** {search_type}

## Summary

| Metric | Value |
|--------|-------|
| Total Clicks | {total_clicks} |
| Total Impressions | {total_impressions} |
| Average CTR | {avg_ctr}% |
| Average Position | {avg_position} |

## Top Queries

| # | Query | Clicks | Impressions | CTR | Position |
|---|-------|--------|-------------|-----|----------|
{queries_table}

## Top Pages

| # | Page | Clicks | Impressions | CTR | Position |
|---|------|--------|-------------|-----|----------|
{pages_table}

## Quick Wins (Position 4-10, High Impressions)

These queries rank on page 1 but below position 3. A small ranking improvement could yield significant traffic gains.

| Query | Position | Impressions | Clicks | CTR | Opportunity |
|-------|----------|-------------|--------|-----|-------------|
{quick_wins_table}

## Device Breakdown

| Device | Clicks | Impressions | CTR | Position |
|--------|--------|-------------|-----|----------|
{device_table}

---
*Data freshness: Search Analytics has a 2-3 day lag. Data available for ~16 months.*
*If this report overlaps 2025-05-13 to 2026-04-27, impressions, CTR, and average position require caution due to the confirmed GSC impressions logging error. Clicks were not affected and affected metrics were not backfilled.*
*Generated {timestamp} via Google Search Console API.*

---

## Supporting file: assets/templates/indexation-status-report.md

# URL Indexation Status Report

**Property:** {property}
**URLs Inspected:** {total_urls}

## Summary

| Status | Count | Percentage |
|--------|-------|-----------|
| Indexed (PASS) | {pass_count} | {pass_pct}% |
| Not Indexed (FAIL) | {fail_count} | {fail_pct}% |
| Neutral | {neutral_count} | {neutral_pct}% |
| Errors | {error_count} | {error_pct}% |

## Detailed Results

| URL | Verdict | Coverage State | Fetch State | Google Canonical | Last Crawl |
|-----|---------|---------------|-------------|-----------------|------------|
{results_table}

## Canonical Mismatches

URLs where Google selected a different canonical than declared:

| URL | User Canonical | Google Canonical |
|-----|---------------|-----------------|
{canonical_mismatches_table}

## Common Issues

| Issue | Count | Priority | Action |
|-------|-------|----------|--------|
{issues_table}

## Rich Results Detected

| URL | Rich Result Type | Status |
|-----|-----------------|--------|
{rich_results_table}

---
*URL Inspection API: 2,000 inspections/day per site, 600/min.*
*Generated {timestamp} via Google Search Console URL Inspection API.*

---

## Supporting file: LICENSE.txt

```text
MIT License - see repository root LICENSE file for complete terms.

Copyright (c) 2026 AgriciDaniel
https://github.com/AgriciDaniel/claude-seo
```

---

## Supporting file: references/auth-setup.md

# Google API Authentication Setup

## Overview

Three credential types serve different APIs:

| Type | Used By | Cost |
|------|---------|------|
| **API Key** | PageSpeed Insights, CrUX, CrUX History, Knowledge Graph | Free |
| **Service Account** | Search Console, Indexing API, GA4 | Free |
| **Both** | Full seo-google skill | Free |

## Step 1: Create a Google Cloud Project

1. Go to [console.cloud.google.com](https://console.cloud.google.com)
2. Click **Select a project** > **New Project**
3. Name it (e.g., "Claude SEO") and note the project ID
4. Select the project after creation

## Step 2: Enable APIs

Navigate to **APIs & Services > Library** and enable:

| API | Required For |
|-----|-------------|
| Google Search Console API | GSC Search Analytics, URL Inspection, Sitemaps |
| PageSpeed Insights API | PSI Lighthouse lab data |
| Chrome UX Report API | CrUX field data + History |
| Web Search Indexing API | Indexing API v3 |
| Google Analytics Data API | GA4 organic traffic |
| Knowledge Graph Search API | Entity verification (optional) |

## Step 3: Create an API Key

1. **APIs & Services > Credentials > Create Credentials > API key**
2. Click **Restrict key**:
   - Under **API restrictions**, select: PageSpeed Insights API, Chrome UX Report API, Knowledge Graph Search API
3. Copy the generated API key and store it securely

## Step 4: Create a Service Account

1. **IAM & Admin > Service Accounts > Create Service Account**
2. Name: `claude-seo` (or similar)
3. Skip optional permissions steps
4. Click on the created service account > **Keys > Add Key > Create new key > JSON**
5. Download the JSON file and store it securely (e.g., `~/.config/claude-seo/service_account.json`)

The JSON file looks like:
```json
{
  "type": "service_account",
  "project_id": "your-project-id",
  "private_key_id": "...",
  "private_key": "<service-account-private-key>",
  "client_email": "<service-account-identifier>",
  "client_id": "...",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token"
}
```

The `client_email` field is what you add to GSC and GA4.

## Step 5: Grant Search Console Access

1. Go to [Google Search Console](https://search.google.com/search-console)
2. Select your property
3. **Settings > Users and permissions > Add user**
4. Paste the service account `client_email`
5. Set permission level:
   - **Full** for read-only (Search Analytics, URL Inspection, Sitemaps)
   - **Owner** if you also need the Indexing API

## Step 6: Grant GA4 Access

1. Go to [Google Analytics](https://analytics.google.com)
2. **Admin > Property Access Management > Add users** (the + icon)
3. Paste the service account `client_email`
4. Set role: **Viewer** (minimum for read-only reporting)
5. Note the numeric property ID from **Admin > Property Details** (e.g., `123456789`)

## Step 7: Create Config File

```bash
mkdir -p ~/.config/claude-seo
```

Save to `~/.config/claude-seo/google-api.json`:

```json
{
  "service_account_path": "~/.config/claude-seo/service_account.json",
  "api_key": "<GOOGLE_API_KEY>",
  "default_property": "sc-domain:example.com",
  "ga4_property_id": "properties/123456789"
}
```

### Property URL Formats

| Format | Example | When to Use |
|--------|---------|-------------|
| Domain property | `sc-domain:example.com` | Covers all URLs on the domain (recommended) |
| URL-prefix property | `https://example.com/` | Covers only that specific prefix |

## Step 8: Verify Setup

```bash
claude-seo run google_auth.py --check
```

Expected output at Tier 2 (full):
```
Credential Tier: 2 -- Full (API key + Service Account + GA4)

  [OK] PageSpeed Insights v5
  [OK] Chrome UX Report (CrUX) API
  [OK] CrUX History API
  [OK] Google Search Console API
       Service account: configured
  [OK] Google Indexing API v3
  [OK] GA4 Data API v1beta
```

## Environment Variable Alternatives

Instead of (or in addition to) the config file:

| Variable | Purpose |
|----------|---------|
| `GOOGLE_API_KEY` | API key for PSI/CrUX |
| `GOOGLE_APPLICATION_CREDENTIALS` | Path to service account JSON |
| `GA4_PROPERTY_ID` | GA4 property (e.g., `properties/123456789`) |
| `GSC_PROPERTY` | Default GSC property (e.g., `sc-domain:example.com`) |

## OAuth Scopes Used

| Scope | APIs |
|-------|------|
| `https://www.googleapis.com/auth/webmasters.readonly` | GSC (read) |
| `https://www.googleapis.com/auth/webmasters` | GSC (read/write, needed for sitemap submission) |
| `https://www.googleapis.com/auth/indexing` | Indexing API |
| `https://www.googleapis.com/auth/analytics.readonly` | GA4 (read) |

## Troubleshooting

| Error | Fix |
|-------|-----|
| `403 Forbidden` on GSC | Service account email not added to GSC property, or wrong permission level |
| `403 Forbidden` on GA4 | Service account email not added to GA4 property as Viewer |
| `404 Not Found` on GSC | Wrong property URL format. Use `sc-domain:` or include trailing slash for URL-prefix |
| `404 Not Found` on CrUX | Site has insufficient Chrome traffic. Not a credentials issue. |
| `429 Rate Limit` | Wait and retry. See rate-limits-quotas.md for per-API limits |
| `API not enabled` | Enable the specific API in GCP Console > APIs & Services > Library |

---

## Supporting file: references/dma-consent-mode-v2.md

# DMA + Consent Mode v2 — click-through impact diagnostic

EU traffic flowing through Google Search has been subject to the
**Digital Markets Act** since 2024-03-07. The DMA limits how Google
can use behavioural data and forces consent-mode v2 compliance for
GA4 + Ads in EU. The operational effect for SEO audits:

- EU click-through-rate (CTR) data from Search Console is materially
  noisier after the DMA enforcement date. Year-over-year CTR
  comparisons across that boundary are not apples-to-apples.
- GA4 organic-traffic reports for EU users show systematic
  under-reporting when consent-mode v2 is configured as "denied for
  ad_storage, granted for analytics_storage" (the typical EU default).
  Conversion modelling fills the gap, but the raw counts are lower
  than pre-2024.

## What the seo-google skill should do

1. **When pulling GSC search analytics for an EU-targeted property**,
   note: "EU CTR comparisons before/after 2024-03-07 are not
   apples-to-apples (DMA + consent-mode v2 took effect)."
2. **When pulling GA4 organic traffic**, surface the consent-mode
   configuration if the GA4 admin API exposes it. If the property
   has `eu_data_collection_disabled` or denies ad_storage by default,
   note "EU traffic counts are conservative; conversion-modelled
   uplift may apply."
3. **Do not lecture the user on cookie consent UX** — that's a legal
   team / engineering concern outside SEO scope. Just attach the
   diagnostic note.

## Required GA4 / Consent Mode setup the audit should check for

- GA4 4.x consent-mode v2 wired up (`gtag('consent', 'default', ...)`
  before any pageview).
- `ads_data_redaction` flag set on EU traffic.
- Server-side tagging at consent transitions (recommended pattern;
  not legally required).

## Softening cookieless-attribution warnings

Google **abandoned** third-party cookie deprecation in July 2024 and
confirmed in April 2025 that Chrome will not ship a standalone cookie
prompt. The "cookieless future" framing is no longer urgent.

For audits as of May 2026:

- Do NOT recommend "switch to cookieless attribution" as a priority.
- DO recommend "implement consent-mode v2 + server-side tagging" for
  EU compliance + signal-loss recovery.
- Privacy Sandbox APIs are still available, but optional. Mention
  only if the audit subject is consumer-facing and has heavy
  retargeting dependence.

## Primary sources

- DMA enforcement: https://digital-markets-act.ec.europa.eu/
- Google's third-party cookie reversal: https://privacysandbox.com/news/privacy-sandbox-update/
- Consent Mode v2, EEA consent updates: https://support.google.com/google-ads/answer/13695607

Last verified: 2026-05-17.

---

## Supporting file: references/ga4-data-api.md

# GA4 Data API v1beta Reference

## Overview

The Google Analytics Data API v1beta provides programmatic access to GA4 report data. For SEO, the primary use case is organic traffic analysis.

**Base URL:** `https://analyticsdata.googleapis.com/v1beta`

## Key Methods

| Method | Description |
|--------|-------------|
| `properties.runReport` | Run a standard report |
| `properties.batchRunReports` | Up to 5 reports in one call |
| `properties.runRealtimeReport` | Last 30 minutes of data |
| `properties.getMetadata` | Available dimensions and metrics |
| `properties.checkCompatibility` | Verify dimension/metric combinations |

## runReport Request

```json
{
  "property": "properties/123456789",
  "dimensions": [
    { "name": "date" },
    { "name": "landingPage" }
  ],
  "metrics": [
    { "name": "sessions" },
    { "name": "totalUsers" }
  ],
  "dateRanges": [
    { "startDate": "28daysAgo", "endDate": "yesterday" }
  ],
  "dimensionFilter": {
    "filter": {
      "fieldName": "sessionDefaultChannelGroup",
      "stringFilter": {
        "matchType": "EXACT",
        "value": "Organic Search"
      }
    }
  },
  "orderBys": [
    { "metric": { "metricName": "sessions" }, "desc": true }
  ],
  "limit": 100,
  "returnPropertyQuota": true
}
```

## SEO-Relevant Dimensions

| Dimension | Description |
|-----------|-------------|
| `date` | Date in YYYYMMDD format |
| `pagePath` | Page path (e.g., `/blog/post`) |
| `landingPage` | Entry page path |
| `landingPagePlusQueryString` | Entry page with query params |
| `fullPageUrl` | Full page URL |
| `pageTitle` | Page title |
| `sessionSource` | Traffic source (e.g., `google`) |
| `sessionMedium` | Traffic medium (e.g., `organic`) |
| `sessionDefaultChannelGroup` | Channel grouping (e.g., `Organic Search`) |
| `country` | User country |
| `deviceCategory` | `desktop`, `mobile`, `tablet` |
| `hostName` | Domain name |
| `pageReferrer` | Referrer URL |

## SEO-Relevant Metrics

| Metric | Description |
|--------|-------------|
| `sessions` | Number of sessions |
| `totalUsers` | Total unique users |
| `newUsers` | First-time users |
| `activeUsers` | Users with engagement |
| `screenPageViews` | Page views |
| `bounceRate` | Bounce rate (0-1, multiply by 100 for %) |
| `averageSessionDuration` | Avg duration in seconds |
| `engagementRate` | Engaged session rate (0-1) |
| `keyEvents` | Key events (replaced deprecated `conversions`) |
| `eventCount` | Total event count |

## Filter Expressions

### String Filter

```json
{
  "filter": {
    "fieldName": "sessionDefaultChannelGroup",
    "stringFilter": {
      "matchType": "EXACT",
      "value": "Organic Search"
    }
  }
}
```

Match types: `EXACT`, `BEGINS_WITH`, `ENDS_WITH`, `CONTAINS`, `FULL_REGEXP`, `PARTIAL_REGEXP`

### Combining Filters

```json
{
  "andGroup": {
    "expressions": [
      { "filter": { "fieldName": "country", "stringFilter": { "matchType": "EXACT", "value": "US" }}},
      { "filter": { "fieldName": "deviceCategory", "stringFilter": { "matchType": "EXACT", "value": "mobile" }}}
    ]
  }
}
```

Also supports `orGroup` and `notExpression`.

## Date Range Shortcuts

| Value | Meaning |
|-------|---------|
| `today` | Current day |
| `yesterday` | Previous day |
| `NdaysAgo` | N days ago (e.g., `28daysAgo`) |
| `YYYY-MM-DD` | Specific date |

Up to 4 date ranges per request (for period-over-period comparison).

## Token-Based Quotas

| Quota | Limit | Scope |
|-------|-------|-------|
| Daily tokens | 25,000 | Per property per project |
| Hourly tokens | 5,000 | Per property per project |
| Concurrent requests | 10 | Per property per project |
| Hourly tokens (project-wide) | 1,250 | Per project per property per hour |

Set `returnPropertyQuota: true` to monitor consumption. Simple reports cost ~1-10 tokens; complex ones up to ~100.

## Python Example

```python
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    DateRange, Dimension, Filter, FilterExpression,
    Metric, OrderBy, RunReportRequest,
)
from google.oauth2 import service_account

credentials = service_account.Credentials.from_service_account_file(
    "service_account.json",
    scopes=["https://www.googleapis.com/auth/analytics.readonly"],
)

client = BetaAnalyticsDataClient(credentials=credentials)

request = RunReportRequest(
    property="properties/123456789",
    dimensions=[Dimension(name="landingPage")],
    metrics=[Metric(name="sessions"), Metric(name="totalUsers")],
    date_ranges=[DateRange(start_date="28daysAgo", end_date="yesterday")],
    dimension_filter=FilterExpression(
        filter=Filter(
            field_name="sessionDefaultChannelGroup",
            string_filter=Filter.StringFilter(
                match_type=Filter.StringFilter.MatchType.EXACT,
                value="Organic Search",
            ),
        )
    ),
    order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="sessions"), desc=True)],
    limit=50,
    return_property_quota=True,
)

response = client.run_report(request)
for row in response.rows:
    print(f"{row.dimension_values[0].value}: {row.metric_values[0].value} sessions")
```

## Authentication
- **Scope:** `https://www.googleapis.com/auth/analytics.readonly`
- Service account must have **Viewer** role in GA4 property
- Add via GA4 Admin > Property Access Management

---

## Supporting file: references/indexing-api.md

# Google Indexing API v3 Reference

## Overview

The Indexing API allows you to notify Google when pages are added or removed.

**IMPORTANT:** Use the Indexing API only for eligible URLs with **JobPosting** or **BroadcastEvent embedded in VideoObject** structured data. For a few ordinary URLs, use URL Inspection; for many ordinary URLs, use sitemaps. A `URL_UPDATED` notification only means Google may recrawl soon and gives no indexing benefit for other page types.

## Endpoints

### Publish Notification

**`POST https://indexing.googleapis.com/v3/urlNotifications:publish`**

```json
{
  "url": "https://example.com/jobs/software-engineer",
  "type": "URL_UPDATED"
}
```

| Field | Values |
|-------|--------|
| `url` | The fully qualified URL |
| `type` | `URL_UPDATED` (page added/changed), `URL_DELETED` (page removed) |

**Response:**
```json
{
  "urlNotificationMetadata": {
    "url": "https://example.com/jobs/software-engineer",
    "latestUpdate": {
      "url": "https://example.com/jobs/software-engineer",
      "type": "URL_UPDATED",
      "notifyTime": "2026-03-27T10:00:00Z"
    }
  }
}
```

### Get Notification Metadata

**`GET https://indexing.googleapis.com/v3/urlNotifications/metadata?url={ENCODED_URL}`**

Returns `latestUpdate` and `latestRemove` timestamps for the URL.

### Batch Requests

**`POST https://indexing.googleapis.com/batch`**

Up to **100 URLs** per batch request using `multipart/mixed` format:

```
POST /batch HTTP/1.1
Content-Type: multipart/mixed; boundary=batch_boundary

--batch_boundary
Content-Type: application/http
Content-ID: <item1>

POST /v3/urlNotifications:publish HTTP/1.1
Content-Type: application/json

{"url": "https://example.com/jobs/1", "type": "URL_UPDATED"}
--batch_boundary
Content-Type: application/http
Content-ID: <item2>

POST /v3/urlNotifications:publish HTTP/1.1
Content-Type: application/json

{"url": "https://example.com/jobs/2", "type": "URL_UPDATED"}
--batch_boundary--
```

## Authentication

- **Scope:** `https://www.googleapis.com/auth/indexing`
- **Auth type:** Service account (OAuth 2.0)
- The service account must be **Owner** in Google Search Console for the target domain

## Quotas

| Limit | Value | Scope |
|-------|-------|-------|
| Publish requests | **200/day** (default) | Per project |
| Read-only requests | 180/min | Per project |
| Total requests | 380/min | Per project |

Request a quota increase: [Indexing API Quota Increase Form](https://developers.google.com/search/apis/indexing-api/v3/quota)

## Error Codes

| Code | Meaning | Action |
|------|---------|--------|
| 400 | Malformed URL or request body | Check URL format |
| 403 | Permission denied or quota exceeded | Add service account as Owner in GSC, or check quota |
| 429 | Rate limit exceeded | Back off and retry with exponential delay |
| 500/503 | Server error | Retry with exponential backoff |

## Best Practices

1. Submit only URLs with actual content changes (don't spam updates)
2. Use `URL_DELETED` only when a page is permanently removed (returns 404/410)
3. Track daily quota usage -- the 200/day limit resets at midnight Pacific Time
4. For large-scale indexing, use XML sitemaps + Search Console instead
5. Batch requests count individually against the daily quota (100 batch = 100 quota)

---

## Supporting file: references/keyword-planner-api.md

# Google Ads API - Keyword Planner Reference

Gold-standard source for keyword search volume. DataForSEO gets its volume data from Google Ads -- this cuts out the middleman.

## Prerequisites (More Complex Than Other Google APIs)

1. **Google Ads Manager Account** -- create at ads.google.com (free to create)
2. **Developer Token** -- apply at Google Ads API Center (requires Basic access approval)
3. **OAuth 2.0 credentials** -- reuse existing OAuth client from seo-google config
4. **For exact volumes**: Run a minimal campaign (~$5-10/day). Without spend, volumes are bucketed ranges ("1K-10K")

## Key Methods

### GenerateKeywordIdeas
Generate keyword suggestions from seed terms.

**Returns per keyword:**
- `text`: Keyword string
- `avg_monthly_searches`: Average monthly volume (exact if spending, bucketed if not)
- `competition`: LOW / MEDIUM / HIGH (for ads, not organic)
- `competition_index`: 0-100 competition score
- `low_top_of_page_bid_micros`: ~20th percentile CPC in micros
- `high_top_of_page_bid_micros`: ~80th percentile CPC in micros
- `monthly_search_volumes[]`: Per-month volume for last 12 months

### GenerateKeywordHistoricalMetrics
Get volume data for specific keywords.

Same return fields as above but for exact keyword list instead of suggestions.

### GenerateKeywordForecastMetrics
Predict clicks, impressions, and cost for keywords.

## Configuration

Add to `~/.config/claude-seo/google-api.json`:

```json
{
  "ads_developer_token": "YOUR_DEV_TOKEN",
  "ads_customer_id": "123-456-7890",
  "ads_login_customer_id": "123-456-7890"
}
```

## Rate Limits

- Keyword Planning requests are more strictly rate-limited than other Ads API services
- Exact QPM/QPS not publicly documented
- Google recommends caching results

## Python Library

```bash
pip install google-ads
```

Uses `google-ads` library (separate from `google-api-python-client`).

## Important Notes

- **Volume accuracy**: Without active ad spend, Google returns bucketed ranges ("1K-10K", "10K-100K") instead of exact numbers like "14,800"
- **Competition score**: Measures advertiser competition for ads, NOT organic ranking difficulty
- **CPC bids**: Reflect what advertisers pay, useful for estimating keyword commercial value
- **Location targeting**: Use location IDs (2840 = United States, 2826 = United Kingdom)
- **Language targeting**: Use language IDs (1000 = English, 1003 = Spanish)

---

## Supporting file: references/nlp-api.md

# Google Cloud Natural Language API Reference

Use NLP analysis as an internal diagnostic for entity coverage, content sentiment, and topic classification. Do not present it as Google's E-E-A-T score, a ranking factor, or a proxy for numeric E-E-A-T weights.

## Endpoint

Entities use `POST https://language.googleapis.com/v1/documents:analyzeEntities`.
Sentiment, classification, and moderation use
`POST https://language.googleapis.com/v2/documents:annotateText`.

Send the API key in the `X-Goog-Api-Key` header, not in the URL.

## Features

| Feature | What It Does | SEO Use |
|---------|-------------|---------|
| `extractEntities` | Extract people, orgs, places, events with salience scores | Topic coverage depth, entity optimization |
| `extractDocumentSentiment` | Document + sentence-level sentiment score/magnitude | Content tone assessment |
| `classifyText` | Map content to 700+ Google categories | Topic relevance verification |
| `moderateText` | Content safety/moderation categories | Content quality flags |

## Entity Types

PERSON, LOCATION, ORGANIZATION, EVENT, WORK_OF_ART, CONSUMER_GOOD, OTHER, PHONE_NUMBER, ADDRESS, DATE, NUMBER, PRICE

Each entity includes:
- `name`: Entity text
- `type`: Entity type
- `salience`: Importance score (0-1, higher = more relevant)
- `sentiment`: Per-entity sentiment (score + magnitude)
- `metadata`: Wikipedia URL, MID (Knowledge Graph ID)
- `mentions`: Occurrences in the text

## Sentiment Scoring

- **Score**: -1.0 (negative) to +1.0 (positive)
- **Magnitude**: 0 to infinity (emotional intensity, higher = more emotional)
- Neutral content: score ~0, low magnitude
- Mixed content: score ~0, HIGH magnitude (both positive and negative)

## Pricing

| Feature | Free/month | Paid (per 1K chars) |
|---------|-----------|-------------------|
| Entity Analysis | 5,000 units | $0.001 |
| Sentiment Analysis | 5,000 units | $0.001 |
| Content Classification | 30,000 units | $0.002 |
| Text Moderation | 50,000 units | $0.0005 |

One "unit" = 1,000 characters. Free tier resets monthly.

## Enable the API

1. Go to [console.cloud.google.com/apis/library](https://console.cloud.google.com/apis/library)
2. Search for "Cloud Natural Language API"
3. Click Enable
4. **Billing must be enabled** on the project (free tier still applies)

Uses the same API key as PSI/CrUX.

---

## Supporting file: references/pagespeed-crux-api.md

# PageSpeed Insights v5 + CrUX API Reference

## Table of Contents
1. [PageSpeed Insights v5](#pagespeed-insights-v5)
2. [CrUX API (Daily)](#crux-api-daily)
3. [CrUX History API (Weekly)](#crux-history-api-weekly)
4. [Core Web Vitals Thresholds](#core-web-vitals-thresholds)

---

## PageSpeed Insights v5

**Endpoint:** `GET https://www.googleapis.com/pagespeedonline/v5/runPagespeed`

### Parameters

| Param | Type | Description |
|-------|------|-------------|
| `url` | string | Required. URL to analyze. |
| `category` | string | `ACCESSIBILITY`, `BEST_PRACTICES`, `PERFORMANCE`, `SEO`. Can specify multiple. |
| `strategy` | string | `DESKTOP` or `MOBILE` (default). |
| `locale` | string | Locale for text (e.g., `en`). |
| `key` | string | API key. Optional but recommended for quota. |

### Response Structure

```
{
  "id": "https://example.com/",
  "loadingExperience": { ... },        // URL-level CrUX data
  "originLoadingExperience": { ... },  // Origin-level CrUX data
  "lighthouseResult": {
    "categories": {
      "performance": { "score": 0.85 },
      "accessibility": { "score": 0.92 },
      "best-practices": { "score": 0.88 },
      "seo": { "score": 0.95 }
    },
    "audits": { ... }                  // Individual audit results
  },
  "analysisUTCTimestamp": "2026-03-27T..."
}
```

### Field Data Metrics (in loadingExperience)

| PSI Key | CrUX Metric | Unit |
|---------|-------------|------|
| `LARGEST_CONTENTFUL_PAINT_MS` | LCP | ms |
| `INTERACTION_TO_NEXT_PAINT` | INP | ms |
| `CUMULATIVE_LAYOUT_SHIFT_SCORE` | CLS | unitless |
| `FIRST_CONTENTFUL_PAINT_MS` | FCP | ms |
| `EXPERIMENTAL_TIME_TO_FIRST_BYTE` | TTFB | ms |

Each metric contains: `percentile` (p75), `distributions[]` ({min, max, proportion}), `category` (FAST/AVERAGE/SLOW/NONE).

### Key Lighthouse Audit IDs

`first-contentful-paint`, `largest-contentful-paint`, `total-blocking-time`, `cumulative-layout-shift`, `speed-index`, `interactive`

### Rate Limits
- 25,000 QPD with API key
- 240 QPM
- Free, no billing required

### Note on Field Data Migration
Google is migrating CrUX field data out of PSI. For field data, prefer the CrUX API directly. Use PSI primarily for Lighthouse lab data.

---

## CrUX API (Daily)

**Endpoint:** `POST https://chromeuxreport.googleapis.com/v1/records:queryRecord`

Send the API key in the `X-Goog-Api-Key` header, not in the URL.

### Request

```json
{
  "origin": "https://example.com",
  "formFactor": "PHONE",
  "metrics": ["largest_contentful_paint", "interaction_to_next_paint", "cumulative_layout_shift"]
}
```

| Field | Description |
|-------|-------------|
| `origin` | Origin URL (mutually exclusive with `url`) |
| `url` | Specific page URL (mutually exclusive with `origin`) |
| `formFactor` | `DESKTOP`, `PHONE`, `TABLET` (optional, omit for all) |
| `metrics` | Array of metric names (optional, omit for all) |

### Available Metrics

| Metric | Type | Notes |
|--------|------|-------|
| `largest_contentful_paint` | int (ms) | Core Web Vital |
| `largest_contentful_paint_resource_type` | fractions | LCP resource type, added January 2025 |
| `largest_contentful_paint_image_time_to_first_byte` | int (ms) | LCP image subpart, added January 2025 |
| `largest_contentful_paint_image_resource_load_delay` | int (ms) | LCP image subpart, added January 2025 |
| `largest_contentful_paint_image_resource_load_duration` | int (ms) | LCP image subpart, added January 2025 |
| `largest_contentful_paint_image_element_render_delay` | int (ms) | LCP image subpart, added January 2025 |
| `interaction_to_next_paint` | int (ms) | Core Web Vital (replaced FID) |
| `cumulative_layout_shift` | **string** | Core Web Vital. **String-encoded!** Parse carefully. |
| `first_contentful_paint` | int (ms) | |
| `experimental_time_to_first_byte` | int (ms) | |
| `round_trip_time` | int (ms) | Replaced effectiveConnectionType (Feb 2025) |
| `navigation_types` | fractions | navigate, navigate_cache, reload, etc. |
| `form_factors` | fractions | desktop/phone/tablet distribution |

### Response

```json
{
  "record": {
    "key": { "origin": "https://example.com" },
    "metrics": {
      "largest_contentful_paint": {
        "histogram": [
          { "start": 0, "end": 2500, "density": 0.72 },
          { "start": 2500, "end": 4000, "density": 0.18 },
          { "start": 4000, "density": 0.10 }
        ],
        "percentiles": { "p75": 2100 }
      },
      "cumulative_layout_shift": {
        "percentiles": { "p75": "0.05" }
      }
    },
    "collectionPeriod": {
      "firstDate": { "year": 2026, "month": 2, "day": 27 },
      "lastDate": { "year": 2026, "month": 3, "day": 26 }
    }
  }
}
```

### Important
- **CLS p75 is a string** (e.g., `"0.05"` not `0.05`). Always parse as float from string.
- Last histogram bin has **no `end`** (extends to infinity).
- Densities sum to approximately 1.0.
- **404** = no data (insufficient Chrome traffic). Not an auth error.
- Updated daily ~04:00 UTC with ~2 day lag.

### Rate Limits
- 150 QPM shared between CrUX and CrUX History APIs
- Free, no paid increase available

---

## CrUX History API (Weekly)

**Endpoint:** `POST https://chromeuxreport.googleapis.com/v1/records:queryHistoryRecord`

Send the API key in the `X-Goog-Api-Key` header, not in the URL.

Same request format as CrUX API. Returns up to **25 weekly collection periods**.

### Response Differences from CrUX API

Instead of single values, returns timeseries:

```json
{
  "record": {
    "metrics": {
      "largest_contentful_paint": {
        "histogramTimeseries": [
          { "start": 0, "end": 2500, "densities": [0.70, 0.71, 0.72, ...] },
          { "start": 2500, "end": 4000, "densities": [0.19, 0.18, 0.18, ...] },
          { "start": 4000, "densities": [0.11, 0.11, 0.10, ...] }
        ],
        "percentilesTimeseries": {
          "p75s": [2200, 2150, 2100, ...]
        }
      }
    },
    "collectionPeriods": [
      {
        "firstDate": { "year": 2025, "month": 9, "day": 29 },
        "lastDate": { "year": 2025, "month": 10, "day": 26 }
      },
      ...
    ]
  }
}
```

### NaN Handling
- `"NaN"` string for densities in ineligible periods
- `null` for percentile values in ineligible periods
- Always check for these before numeric operations

### Update Schedule
- Updated **Mondays** ~04:00 UTC
- Each period = 28-day rolling average ending on a Sunday

---

## Core Web Vitals Thresholds

Current as of 2026-07-09; no threshold changes were announced. INP replaced FID on March 12, 2024.

| Metric | Good | Needs Improvement | Poor |
|--------|------|-------------------|------|
| **LCP** | ≤ 2,500ms | 2,500–4,000ms | > 4,000ms |
| **INP** | ≤ 200ms | 200–500ms | > 500ms |
| **CLS** | ≤ 0.1 | 0.1–0.25 | > 0.25 |
| **FCP** | ≤ 1,800ms | 1,800–3,000ms | > 3,000ms |
| **TTFB** | ≤ 800ms | 800–1,800ms | > 1,800ms |

FID was removed from Chrome's field-data tools (CrUX, PSI) on September 9, 2024. Never reference FID in outputs.

---

## Supporting file: references/rate-limits-quotas.md

# Google API Rate Limits & Quotas

## Consolidated Quota Table

| API | Per-Minute | Per-Day | Cost | Auth Type | Scope |
|-----|-----------|---------|------|-----------|-------|
| GSC Search Analytics | 1,200 QPM/user, 1,200 QPM/site | 30M QPD/project | Free | Service Account | Per user + per site |
| GSC URL Inspection | 600 QPM/site | 2,000 QPD/site | Free | Service Account | Per site |
| GSC Sitemaps | Standard | Standard | Free | Service Account | Per site |
| PageSpeed Insights v5 | 240 QPM | 25,000 QPD | Free | API Key | Per project |
| CrUX API | 150 QPM (shared) | Unlimited | Free | API Key | Per project |
| CrUX History API | 150 QPM (shared with CrUX) | Unlimited | Free | API Key | Per project |
| Indexing API | 380 RPM total, 180 read/min | 200 publish/day | Free | Service Account | Per project |
| GA4 Data API | 10 concurrent | ~25K tokens/day | Free | Service Account | Per property/project |
| Knowledge Graph | -- | 100,000 QPD | Free | API Key | Per project |
| Custom Search | -- | 10,000 QPD max | 100 free, $5/1K | API Key | Per project |
| Web Risk | 6,000 QPM | 100K/month | Free tier | API Key | Per project |

**Key distinction:** "Per site" quotas are scoped to a specific GSC property. "Per project" quotas are shared across all properties in a GCP project. "Per user" quotas are per authenticated user (service account).

## Exponential Backoff Strategy

When receiving 429 or 5xx errors:

```
Attempt 1: wait 1 second
Attempt 2: wait 2 seconds
Attempt 3: wait 4 seconds
Attempt 4: wait 8 seconds
Attempt 5: wait 16 seconds
Max: give up after 5 retries
```

Add random jitter (0-500ms) to each wait to avoid thundering herd.

## Common Error Codes

| Code | Meaning | Applies To | Action |
|------|---------|------------|--------|
| 400 | Bad request | All | Check URL format, request body |
| 401 | Unauthorized | Service Account APIs | Refresh credentials |
| 403 | Forbidden | GSC, GA4, Indexing | Check permissions (service account access) |
| 404 | Not found | CrUX, GSC | Insufficient traffic (CrUX) or invalid property (GSC) |
| 429 | Rate limited | All | Backoff and retry. Check Retry-After header. |
| 500 | Server error | All | Retry with backoff |
| 503 | Service unavailable | All | Retry with backoff |

## Retry-After Header

Some Google APIs return a `Retry-After` header with 429 responses. When present, use this value (in seconds) instead of exponential backoff.

## GA4 Token Budgeting

GA4 uses a token system rather than simple request counts:
- Simple 1-dimension, 1-metric report: ~1-5 tokens
- Complex multi-dimension, multi-metric: ~10-100 tokens
- Set `returnPropertyQuota: true` to monitor consumption
- Daily limit: 25,000 tokens per property per project
- Hourly limit: 5,000 tokens per property per project
- Concurrent: max 10 simultaneous requests

## CrUX Shared Quota

The CrUX API and CrUX History API share the same 150 QPM quota per project. Plan accordingly if querying both APIs in the same workflow.

## Cost Summary

**All APIs used by seo-google are free** at normal usage levels. No billing is required for:
- PSI, CrUX, CrUX History (API key, unlimited free)
- GSC (service account, 30M QPD)
- Indexing API (service account, 200 publish/day)
- GA4 Data API (service account, 25K tokens/day)
- Knowledge Graph (API key, 100K QPD)

Only Custom Search and Web Risk have paid tiers at high volumes.

---

## Supporting file: references/search-console-api.md

# Google Search Console API Reference

## Table of Contents
1. [Search Analytics API](#search-analytics-api)
2. [URL Inspection API](#url-inspection-api)
3. [Sitemaps API](#sitemaps-api)
4. [Sites API](#sites-api)

---

## Search Analytics API

**Endpoint:** `POST https://www.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query`

### Request Body

| Field | Type | Description |
|-------|------|-------------|
| `startDate` | string | Required. YYYY-MM-DD format. |
| `endDate` | string | Required. YYYY-MM-DD format. |
| `dimensions` | string[] | `query`, `page`, `country`, `device`, `date`, `searchAppearance` |
| `type` | string | `web` (default), `image`, `video`, `news`, `discover`, `googleNews` |
| `dimensionFilterGroups` | object[] | Array of filter groups (see below) |
| `aggregationType` | string | `auto` (default), `byPage`, `byProperty`, `byNewsShowcasePanel` |
| `rowLimit` | int | 1-25000 (default: 1000) |
| `startRow` | int | Pagination offset (default: 0) |
| `dataState` | string | `final` (default), `all`, `hourly_all` |

### Dimension Filters

```json
{
  "dimensionFilterGroups": [{
    "filters": [{
      "dimension": "query",
      "operator": "contains",
      "expression": "seo"
    }]
  }]
}
```

**Operators:** `contains`, `equals`, `notContains`, `notEquals`, `includingRegex`, `excludingRegex`
- Regex uses RE2 syntax, max 4096 characters

### Response

```json
{
  "rows": [
    {
      "keys": ["seo tools", "https://example.com/tools"],
      "clicks": 150,
      "impressions": 5000,
      "ctr": 0.03,
      "position": 4.2
    }
  ],
  "responseAggregationType": "byPage"
}
```

### Important Notes
- Data has a **2-3 day lag**. Available for approximately 16 months.
- If a date range overlaps 2025-05-13 to 2026-04-27, clicks remain usable but impressions, CTR, and average position require caution due to the confirmed GSC impressions logging error.
- `discover` and `googleNews` types do not support `query` dimension or `position` metric.
- Country codes are **ISO 3166-1 alpha-3** (e.g., `USA`, `GBR`, `DEU`).
- Pagination: increment `startRow` by `rowLimit` until fewer rows returned.
- `rowLimit` is per API request. The claude-seo `--limit` option is a total
  result cap and requests are paged internally at up to 25,000 rows.
- For accurate site totals, omit `query` and `page` dimensions. Query rows can
  omit anonymized low-volume traffic and must not be summed as site totals.
- Only present totals as authoritative when the script returns
  `"totals_complete": true`.

### Rate Limits
- 1,200 QPM per user
- 1,200 QPM per site
- 40,000 QPM / 30,000,000 QPD per project

---

## URL Inspection API

**Endpoint:** `POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect`

### Request

```json
{
  "inspectionUrl": "https://example.com/page",
  "siteUrl": "sc-domain:example.com",
  "languageCode": "en"
}
```

### Response Fields

**`indexStatusResult`:**

| Field | Values |
|-------|--------|
| `verdict` | `PASS`, `FAIL`, `NEUTRAL`, `PARTIAL`, `VERDICT_UNSPECIFIED` |
| `coverageState` | Human-readable coverage description |
| `robotsTxtState` | `ALLOWED`, `DISALLOWED` |
| `indexingState` | `INDEXING_ALLOWED`, `BLOCKED_BY_META_TAG`, `BLOCKED_BY_HTTP_HEADER` |
| `pageFetchState` | `SUCCESSFUL`, `SOFT_404`, `BLOCKED_ROBOTS_TXT`, `NOT_FOUND`, `ACCESS_DENIED`, `SERVER_ERROR`, `REDIRECT_ERROR`, `ACCESS_FORBIDDEN`, `BLOCKED_4XX`, `INTERNAL_CRAWL_ERROR`, `INVALID_URL` |
| `lastCrawlTime` | ISO 8601 timestamp |
| `googleCanonical` | URL Google selected as canonical |
| `userCanonical` | URL declared canonical by the page |
| `crawledAs` | `DESKTOP`, `MOBILE` |

**`richResultsResult`:** Verdict + detected rich result types (e.g. Product, Breadcrumb, Review snippet, Event). Note: FAQPage and HowTo no longer produce rich results (FAQ retired 2026-05-07; HowTo retired 2023), so don't expect them as current rich-result types.

### Rate Limits
- 2,000 QPD / 600 QPM per site
- 10,000,000 QPD / 15,000 QPM per project

---

## Sitemaps API

**Base:** `https://www.googleapis.com/webmasters/v3/sites/{siteUrl}/sitemaps`

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/sitemaps` | List all sitemaps |
| `GET` | `/sitemaps/{feedpath}` | Get specific sitemap |
| `PUT` | `/sitemaps/{feedpath}` | Submit a sitemap |
| `DELETE` | `/sitemaps/{feedpath}` | Delete a sitemap |

### Sitemap Resource

| Field | Description |
|-------|-------------|
| `path` | URL of the sitemap |
| `lastSubmitted` | Last submission timestamp |
| `isPending` | Whether processing is incomplete |
| `isSitemapsIndex` | Whether this is a sitemap index |
| `type` | `sitemap`, `atomFeed`, `rssFeed`, `urlList`, `notSitemap` |
| `warnings` | Warning count |
| `errors` | Error count |
| `contents[]` | Array with `type` (web, image, video, news) and `submitted` count |

---

## Sites API

**Base:** `https://www.googleapis.com/webmasters/v3/sites`

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/sites` | List all verified properties |
| `GET` | `/sites/{siteUrl}` | Get property info |
| `PUT` | `/sites/{siteUrl}` | Add a property |
| `DELETE` | `/sites/{siteUrl}` | Remove a property |

### Property Resource

| Field | Values |
|-------|--------|
| `siteUrl` | Property URL (e.g., `sc-domain:example.com`) |
| `permissionLevel` | `siteOwner`, `siteFullUser`, `siteRestrictedUser`, `siteUnverifiedUser` |

---

## Supporting file: references/supplementary-apis.md

# Supplementary Google APIs for SEO

## Knowledge Graph Search API

Verify brand/entity presence in Google's Knowledge Graph.

**Endpoint:** `GET https://kgsearch.googleapis.com/v1/entities:search`

| Param | Description |
|-------|-------------|
| `query` | Search query |
| `ids` | Specific entity IDs (e.g., `/m/0d6lp`) |
| `languages` | Language codes (e.g., `en`) |
| `types` | Schema.org types to filter (e.g., `Organization`, `Person`) |
| `limit` | Max results (1-500) |
| `key` | API key (required) |

**Response:**
```json
{
  "itemListElement": [{
    "result": {
      "@id": "kg:/m/0d6lp",
      "name": "Google",
      "@type": ["Organization", "Corporation"],
      "description": "Technology company",
      "detailedDescription": {
        "articleBody": "Google LLC is an American...",
        "url": "https://en.wikipedia.org/wiki/Google"
      },
      "image": { "url": "..." },
      "url": "https://www.google.com"
    },
    "resultScore": 4892.5
  }]
}
```

**Use for SEO:** Verify if a brand has a Knowledge Panel, check entity disambiguation, find related entities.

**Quota:** 100,000 reads/day. Free. API key only.

---

## Custom Search JSON API

Programmatic Google search results (limited).

**Endpoint:** `GET https://customsearch.googleapis.com/customsearch/v1`

| Param | Description |
|-------|-------------|
| `key` | API key (required) |
| `cx` | Programmable Search Engine ID (required) |
| `q` | Search query |
| `num` | Results per page (1-10) |
| `start` | Start index (max 91) |
| `dateRestrict` | Date restriction (e.g., `d30` for 30 days) |
| `gl` | Country (e.g., `us`) |
| `lr` | Language restriction |
| `searchType` | `image` for image search |
| `siteSearch` | Restrict to a domain |

**Limitations:**
- Max 100 total results per query (10 pages x 10 results)
- **CLOSED to new customers as of 2025.** Existing customers must migrate by January 2027.
- 100 queries/day free, $5 per 1,000 up to 10,000/day

**For SERP data, prefer DataForSEO** (`/seo dataforseo serp`) which has no such limitations.

---

## Web Risk API

Check if URLs are flagged as unsafe by Google Safe Browsing.

**Endpoint:** `GET https://webrisk.googleapis.com/v1/uris:search`

| Param | Description |
|-------|-------------|
| `threatTypes` | `MALWARE`, `SOCIAL_ENGINEERING`, `UNWANTED_SOFTWARE`, `SOCIAL_ENGINEERING_EXTENDED_COVERAGE` |
| `uri` | URL to check |
| `key` | API key (required) |

**Response (clean URL):** Empty threat object.

**Response (flagged URL):**
```json
{
  "threat": {
    "threatTypes": ["MALWARE"],
    "expireTime": "2026-04-01T00:00:00Z"
  }
}
```

**Use for SEO:** Check if pages are flagged (could explain deindexing), verify competitor safety, audit outbound links.

**Quota:** 6,000 QPM. 100,000/month free tier. Requires billing enabled on GCP project.

---

## Supporting file: references/youtube-api.md

# YouTube Data API v3 Reference

YouTube mentions may correlate with AI visibility in some GEO research, but this file does not include a Google-owned source or methodology for a numeric benchmark. This API provides authoritative YouTube data directly from Google.

## Endpoints Used

| Method | Quota Cost | Description |
|--------|-----------|-------------|
| `search.list` | 100 units | Search for videos matching a query |
| `videos.list` | 1 unit | Get video details, statistics, content |
| `channels.list` | 1 unit | Get channel info, subscriber count |
| `commentThreads.list` | 1 unit | Get top comments on a video |

## Daily Quota

Default: **10,000 units/day** (free). This allows:
- ~100 searches per day, OR
- ~10,000 video/channel lookups per day

## Data Available

### Video Search
- Title, channel, channel ID, published date
- Description (first 300 chars), thumbnail URL
- Views, likes, comments count, duration

### Video Details
- Full description, tags, category ID
- Duration, definition (HD/SD), has captions
- Topic categories (Wikipedia URLs)
- Views, likes, comments, favorites
- Top 10 relevant comments with likes

### Channel Info
- Title, description, custom URL
- Subscriber count, video count, total views
- Country, published date, thumbnail

## Authentication

**API key only** (read-only public data). No OAuth needed.

## Enable the API

1. Go to [console.cloud.google.com/apis/library](https://console.cloud.google.com/apis/library)
2. Search for "YouTube Data API v3"
3. Click Enable

No billing required. The API key you already have for PSI/CrUX works.
