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:
clicksandimpressionsare summed across the variantsctris recomputed asclicks / impressions(0when there are no impressions)positionis the impressions-weighted mean of the variants' positions (an unweighted mean when there are no impressions to weight by)urlis 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 itsurl. If a page's numbers look implausibly small next toGET /sites/{siteId}/performance, that stored URL is probably pointing at a variant. Repoint it withPATCH /pages/{pageId}(gsc_url), or send"gsc_url": nullto 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.