# Query Search Analytics — Google Search Console

> Query Google Search Console search analytics for one property and one date range: clicks, impressions, CTR and average position, optionally grouped by dimensions and filtered. This is the main traffic-reporting tool for a site.

- Key: `google_search_console-retrieve-site-performance-data`
- Type: Action (Read-only)
- Version: 1.1.0
- App: Google Search Console (`google_search_console`) — https://pipedream.com/apps/google-search-console.md
- This page (HTML): https://pipedream.com/apps/google-search-console/actions/retrieve-site-performance-data
- Hints: read-only · open-world
- Source: https://github.com/PipedreamHQ/pipedream/blob/master/components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs

## Description

Query Google Search Console search analytics for one property and one date range: clicks, impressions, CTR and average position, optionally grouped by dimensions and filtered. This is the main traffic-reporting tool for a site.

**Use for** any single-date-range question about how a site performs in Google Search — top queries, top pages, country or device splits, daily or hourly trends, the CTR or average position of a term. Use **Compare Search Analytics** instead for period-over-period questions (month over month, year over year, "did the update hurt us") — it fetches both ranges and joins them for you. Use **Inspect URLs** for index status, canonicals and crawl state; this tool only reports traffic.

**Returns** the API response unchanged — `rows` (each `{ keys, clicks, impressions, ctr, position }`, where `keys` lines up positionally with `dimensions`), `responseAggregationType` and `metadata` — plus `row_count`, `has_more` (the page came back full, so more rows probably exist), `next_start_row` (pass it back as `startRow`) and `returned_totals` (`clicks` and `impressions` summed over the RETURNED rows only).

**Reading the numbers.** `ctr` is a 0-1 fraction (`0.1428` means 14.3%) and `position` is a 1-indexed float where LOWER is better. Both are impression-weighted, so never re-average them across rows — a plain mean is wrong. `returned_totals` is not the property total, least of all when grouping by `query`: Google omits anonymized (rare) queries, so the sum of query rows is materially LESS than the same range grouped by `date`. For a true total, query with no dimensions or with `date`. And check `has_more` before reporting a count or a "top N" — a truncated first page is not the whole answer.

**Scoping to one page or segment.** Set `subdomainFilter` — the filter VALUE, despite the legacy prop name — together with `filterDimension` and `filterOperator`. For "page X only" that is `filterDimension: page`, `filterOperator: equals`, value = the full URL. Selecting a matching URL-prefix property instead is NOT equivalent: a prefix property covers every page under it, so the numbers come back for the whole prefix. Use `advancedDimensionFilters` for multi-condition filters; it is ignored whenever `subdomainFilter` is set.

**Freshness.** The most recent 2-3 days are not final and are omitted under `dataState: final` (the default), so a range ending today comes back one to three rows short. When the user asks for "the last N days" and every day needs a figure, set `dataState: all`.

**Discover has no `query` dimension** (400 "Request for DISCOVER cannot be grouped by query"). When the user asks for Discover queries, do not stop to ask: report Discover pages instead (`searchType: discover`, `dimensions: ["page"]`) and say why.

**Example.** `siteUrl="sc-domain:example.com"`, `startDate="2025-09-01"`, `endDate="2026-08-31"`, `dimensions=["query"]`, `rowLimit=10` returns rows such as `{ keys: ["example brand"], clicks: 41, impressions: 287, ctr: 0.1429, position: 2.4 }` plus `row_count: 10`, `has_more: true` and `next_start_row: 10`.

There is no `fields` parameter — rows are already minimal, so `rowLimit` plus `has_more`/`next_start_row` is the payload lever. Quota: 1,200 queries per minute per site. [See the documentation](https://developers.google.com/webmaster-tools/v1/searchanalytics/query)

## Props

| Prop | Type | Required | Description |
|---|---|---|---|
| `siteUrl` | `string` | Yes | Exact property identifier from List Sites — sc-domain:example.com for a domain property, or a URL-prefix such as https://www.example.com/ (trailing slash; scheme and subdomain must match exactly, or the call 403s). Copy it verbatim, never construct it. For traffic questions prefer the domain property when one exists: it covers all subdomains and protocols. |
| `startDate` | `string` | Yes | First day of the range, inclusive, YYYY-MM-DD Pacific Time — e.g. 2025-09-01. Search Console keeps 16 months; an earlier date returns a 400. The most recent 2-3 days are not final and are omitted unless dataState is all. |
| `endDate` | `string` | Yes | Last day of the range, inclusive, YYYY-MM-DD Pacific Time — e.g. 2026-08-31. Reporting lags 2-3 days, so an end date of today returns nothing for the final days unless dataState is all. |
| `dimensions` | `string[]` | No | How to group the rows; each row's keys lines up positionally with this list, so ["query","device"] yields keys: ["example brand","MOBILE"]. Leave empty for a single totals row. Combination rules, each a 400 if broken: searchAppearance cannot be combined with ANY other dimension (fetch the appearance types alone, then filter by one with filterDimension: searchAppearance); hour cannot be combined with date, requires dataState: hourly_all, and covers at most 10 days; Discover (searchType: discover) has no query dimension. date returns one row per calendar day, zero-impression days included — so count rows with impressions > 0, not rows, for active days. |
| `searchType` | `string` | No | Which Google surface to report on. web (default) is normal Google Search; discover is the Discover feed (no query dimension); googleNews is news.google.com, news is the News tab of Google Search. Sent to the API as type. |
| `aggregationType` | `string` | No | How Google aggregates the metrics. auto (default) lets Google choose — by page when grouping by page, by property otherwise. byPage aggregates by URI, byProperty across the whole property, byNewsShowcasePanel is for News Showcase. byProperty is rejected whenever a page dimension or page filter is present: 400 "'BY_PROPERTY' is not a valid aggregation type in the context of the request." The property-level average position is the byProperty number and does NOT equal the average of the byPage rows. |
| `rowLimit` | `integer` | No | Rows to return in this call. Defaults to 50; API maximum 25000, but keep it at 200 or below — 100 rows is ≈13k characters and 400 rows exceeds the output cap and is spilled to a file you cannot read. Size it to the task: 5-10 for a "top query" answer, 200 for a sweep, then page with startRow: next_start_row while has_more is true (there is no page token). The last page is the one returning fewer rows than rowLimit; an empty final page is normal. |
| `startRow` | `integer` | No | Zero-based index of the first row to return. Omit for the first page, then pass the next_start_row value from the previous response to page forward. |
| `subdomainFilter` | `string` | No | The value to filter on, for ANY dimension — not just subdomains (Query Search Analytics keeps the legacy key subdomainFilter). Combined with filterDimension and filterOperator into one filter, e.g. filterDimension: page, filterOperator: contains, value https://www.example.com/blog/. page expressions match the FULL URL, scheme and host included, not a path. Setting this makes advancedDimensionFilters ignored. |
| `filterDimension` | `string` | No | Dimension the single-filter shortcut applies to; default page. Filtering does not require grouping by the same dimension — you can filter by page while grouping by query. page expressions match the FULL URL, scheme and host included, not a path. |
| `filterOperator` | `string` | No | How the filter value is compared. String comparison is case-insensitive. includingRegex/excludingRegex use RE2 syntax (no lookahead/lookbehind). Default contains. |
| `advancedDimensionFilters` | `string` | No | JSON for multi-condition filtering, used only when the single-filter shortcut is empty. Accepts either a bare array of filters, which is ANDed into one group — e.g. [{"dimension":"country","operator":"equals","expression":"usa"},{"dimension":"device","operator":"equals","expression":"MOBILE"}] — or the raw API dimensionFilterGroups array, e.g. [{"groupType":"and","filters":[{"dimension":"page","operator":"contains","expression":"/blog/"}]}]. The API only supports groupType: "and"; there is no OR. Regex operators use RE2. |
| `dataState` | `string` | No | Which data to include. final (default) returns only finalized data, which lags roughly 2-3 days. all also includes the most recent, not-yet-final days and adds metadata.firstIncompleteDate — use it when the user explicitly wants recent or partial numbers. hourly_all is required by the hour dimension. |

## Run it

**MCP**

```ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { PipedreamClient } from "@pipedream/sdk"

const pd = new PipedreamClient({
  projectId: process.env.PIPEDREAM_PROJECT_ID!,
  clientId: process.env.PIPEDREAM_CLIENT_ID!,
  clientSecret: process.env.PIPEDREAM_CLIENT_SECRET!,
  projectEnvironment: "production",
})

const accessToken = await pd.rawAccessToken

const transport = new StreamableHTTPClientTransport(
  new URL("https://remote.mcp.pipedream.net/v3"),
  {
    requestInit: {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "x-pd-project-id": process.env.PIPEDREAM_PROJECT_ID!,
        "x-pd-environment": "production",
        "x-pd-external-user-id": "{external_user_id}", // any stable ID for this user in your system
        "x-pd-app-slug": "google_search_console",
      },
    },
  },
)

const mcp = new Client({ name: "my-agent", version: "1.0.0" })
await mcp.connect(transport)

const { tools } = await mcp.listTools()

// listTools() hands your model this tool's input schema, so it can
// fill the arguments itself:
const result = await mcp.callTool({
  name: "google_search_console-retrieve-site-performance-data",
  arguments: {
    siteUrl: "Property (siteUrl)",
    startDate: "Start Date (YYYY-MM-DD)",
  },
})
```

**TypeScript**

```ts
import { PipedreamClient } from "@pipedream/sdk"

const pd = new PipedreamClient({
  projectId: process.env.PIPEDREAM_PROJECT_ID!,
  clientId: process.env.PIPEDREAM_CLIENT_ID!,
  clientSecret: process.env.PIPEDREAM_CLIENT_SECRET!,
  projectEnvironment: "production",
})

const result = await pd.actions.run({
  id: "google_search_console-retrieve-site-performance-data",
  externalUserId: "{external_user_id}", // any stable ID for this user in your system
  configuredProps: {
    google_search_console: { authProvisionId: "apn_xxxxxxx" },
    siteUrl: "Property (siteUrl)",
    startDate: "Start Date (YYYY-MM-DD)",
  },
})

console.log(result)
```

**cURL**

```bash
curl -X POST https://api.pipedream.com/v1/connect/{project_id}/actions/run \
  -H "Content-Type: application/json" \
  -H "X-PD-Environment: production" \
  -H "Authorization: Bearer {access_token}" \
  -d '{
    "external_user_id": "{external_user_id}",
    "id": "google_search_console-retrieve-site-performance-data",
    "configured_props": {
      "google_search_console": { "authProvisionId": "apn_xxxxxxx" },
      "siteUrl": "Property (siteUrl)",
      "startDate": "Start Date (YYYY-MM-DD)"
    }
  }'
```

---

- App: https://pipedream.com/apps/google-search-console.md · All apps: https://pipedream.com/apps
