Search Console data

Search Console data

Three endpoints read live Google Search Console data for a connected site — there are no GSC fact tables backing this API, so every call is a real request to Google made while your request waits. Expect roughly a second of latency and budget your per-key rate limit accordingly; none of these are suited to tight polling loops.

Scope: sites:read on all three.


Window parameters

All three endpoints accept the same window controls:

Parameter Type Notes
days integer Trailing window length, 1–365, default 28. Ends 2 days ago — Google Search Console's reporting lag means the most recent 2 days never have complete data. Ignored when start_date/end_date are given.
start_date string (YYYY-MM-DD) Explicit window start. Must be paired with end_date.
end_date string (YYYY-MM-DD) Explicit window end. Span (end_date - start_date + 1) must be ≤ 365 days.

Every response echoes the resolved window back as window: { start_date, end_date }, so you always know exactly what range you got even when you used the days shorthand.

gsc_status

Every response carries a gsc_status field describing whether live data was actually fetched:

gsc_status Meaning What to do
available Google returned data (possibly empty rows if there's genuinely no traffic). Use data normally.
not_indexed The page is known to be un-indexed as of a recent reconciliation (page-level endpoints only), or it's a planned page with no live URL yet. Nothing to fetch — don't retry.
not_connected The site has no active GSC connection. Tell the user to connect Search Console for this site in the MetaMonster dashboard.
unavailable The live call to Google failed (timeout, API error, etc.). Back off and retry later — don't loop tightly on this endpoint.

not_indexed is page-scoped and only short-circuits when the site's index reconciliation is fresh (within the last 7 days); otherwise the endpoint still makes the live call and may come back unavailable or available with empty rows.

Google failures never produce a 5xx — they always resolve to gsc_status: "unavailable" with empty data (and totals: null where applicable), so a non-2xx response from these endpoints means something else (auth, validation, rate limit).


GET /sites/{siteId}/performance

Live per-URL performance across the whole site — one row per URL Google Search Console reported on, restricted to the canonical host. Each row carries page_id when the URL matches one of your organization's pages (null for URLs Google indexed that MetaMonster doesn't track as a page). totals are undimensioned — computed as a separate query, not summed from data — so they're accurate even though per-row query rows drop under Google's anonymization threshold.

URL variants and page_id

Search Console reports https://example.com/, https://example.com/#reviews and https://example.com/?utm_source=nl as three separate rows. They're all the same page here, so page_id is resolved by canonical page key (host + path, no query, no fragment) — every variant of a URL you own comes back with the same page_id rather than null.

The rows themselves stay as Google reported them unless you ask for folding.

group_by=page

Param Value Effect
group_by page Fold every URL variant into one row per page

With group_by=page, each page gets exactly one row:

  • clicks and impressions are summed across the variants
  • ctr is recomputed as clicks / impressions (0 when there are no impressions)
  • position is the impressions-weighted mean of the variants' positions (an unweighted mean when there are no impressions to weight by)
  • url is the page's own canonical URL, not whichever variant Google reported

Rows that don't map to one of your pages still appear, folded by their canonical key, with page_id: null and the canonical key as url.

Folding runs before the row cap, so a page whose traffic is split across a dozen query-string variants is ranked (and kept) on its real total. meta.total then counts folded rows. totals is unaffected either way.

curl -s "https://new.metamonster.ai/api/v1/sites/42/performance?days=28&group_by=page" \
  -H "Authorization: Bearer mm_YOUR_API_KEY"

Ordering and the row cap

Rows are ordered by clicks (descending), with impressions as the tiebreaker. Google returns up to 25,000 URLs for a large site, so there's a hard cap of 1000 rows per response — if the site has more, only the top 1000 by that ordering come back.

meta.total is the count of rows actually returned in this response (so it's always ≤ 1000) — it is not the site's true URL count when truncation happened. meta.truncated is the trust signal: check it, not meta.total, to know whether you're looking at everything. totals always covers the whole site regardless of the cap, so site-wide clicks/impressions stay accurate even on a truncated response.

Response includes Cache-Control: private, max-age=300.

Request

curl -s "https://new.metamonster.ai/api/v1/sites/42/performance?days=28" \
  -H "Authorization: Bearer mm_YOUR_API_KEY"

Response 200

{
  "data": [
    { "url": "https://example.com/pricing", "page_id": 5001, "clicks": 120, "impressions": 5400, "ctr": 0.0222, "position": 11.3 },
    { "url": "https://example.com/old-campaign-page", "page_id": null, "clicks": 4, "impressions": 210, "ctr": 0.019, "position": 22.1 }
  ],
  "totals": { "clicks": 1204, "impressions": 58900, "ctr": 0.0204, "position": 16.8 },
  "gsc_status": "available",
  "window": { "start_date": "2026-07-17", "end_date": "2026-08-13" },
  "meta": { "total": 2, "truncated": false }
}

Errors

Status When
400 invalid_request Bad days/start_date/end_date (out of range, mismatched pairing, span > 365 days), or a group_by value other than page
404 not_found No such site in your organization

GET /pages/{pageId}/queries

Top search queries (by clicks, capped at 50) that surfaced the page's URL in Google's results over the window.

Which URL a page is measured on. Per-page GSC endpoints filter Search Console on one exact URL — the page's gsc_url (derived from Google's own indexed URLs) falling back to its url. If a page's numbers look implausibly small next to GET /sites/{siteId}/performance, that stored URL is probably pointing at a variant. Repoint it with PATCH /pages/{pageId} (gsc_url), or send "gsc_url": null to clear it and let the next reconciliation re-derive it. See Pages.

Request

curl -s "https://new.metamonster.ai/api/v1/pages/5001/queries?days=28" \
  -H "Authorization: Bearer mm_YOUR_API_KEY"

Response 200

{
  "data": [
    { "query": "example pricing", "clicks": 64, "impressions": 1800, "ctr": 0.0356, "position": 8.2 },
    { "query": "example plans and pricing", "clicks": 21, "impressions": 640, "ctr": 0.0328, "position": 12.1 }
  ],
  "gsc_status": "available",
  "window": { "start_date": "2026-07-17", "end_date": "2026-08-13" }
}

Errors

Status When
400 invalid_request Bad window params
404 not_found No such page in your organization

GET /pages/{pageId}/performance

Daily time series for one page — one row per day that actually had data (days with zero activity are absent, not zero-filled), ascending by date — plus undimensioned totals for the whole window. Same gsc_status semantics as /queries.

Request

curl -s "https://new.metamonster.ai/api/v1/pages/5001/performance?start_date=2026-07-01&end_date=2026-07-31" \
  -H "Authorization: Bearer mm_YOUR_API_KEY"

Response 200

{
  "data": [
    { "date": "2026-07-02", "clicks": 6, "impressions": 180, "ctr": 0.0333, "position": 10.9 },
    { "date": "2026-07-03", "clicks": 4, "impressions": 165, "ctr": 0.0242, "position": 11.4 }
  ],
  "totals": { "clicks": 120, "impressions": 5400, "ctr": 0.0222, "position": 11.3 },
  "gsc_status": "available",
  "window": { "start_date": "2026-07-01", "end_date": "2026-07-31" }
}

Errors

Status When
400 invalid_request Bad window params
404 not_found No such page in your organization

Prefer the brief's bundled queries when you can

GET /pages/{pageId}/brief already bundles a 28-day queries section for the page — if you're already calling it (or about to), read GSC from there instead of a separate live call, and reserve /queries and /performance for cases where you specifically need a custom window.