# See which page ranks for each query with the Search Console API.

**Author:** Ivan Misic  
**Published:** 2026-09-01  
**URL:** https://ivanmisic.net/blog/ai-tools/search-console-api-page-query-pairs

**In plain English**

The Search Console API shows which page Google displays for each query. The normal export cannot answer that because it puts queries and pages in separate tables.

Setup takes about fifteen minutes in the browser: create a Cloud project, enable the Search Console API, make a service account, download its JSON key, and add that account as a restricted user on your property. Then ask for `["page", "query"]` as your dimensions.

If you would rather not build it, the search-visibility plugin does the fetch and the analysis for you. Two things the API still will not give you: the coverage report and the links report. Both stay manual, because Google publishes no API for either.

Reading Search Console on a schedule is better than opening it only when something feels wrong. Traffic dipped, a page vanished, someone asked. You set a date range, squint at the chart, download a zip, and try to reconstruct what changed.

The setup takes fifteen minutes, and it gives you the most useful thing the export leaves out.

## The question the export cannot answer

Open a Performance export and you get one file of your top queries and another of your top pages. There is no file with both.

So this has no answer in the zip: for a query I care about, which of my pages is Google actually showing?

You need that the moment two of your own posts cover overlapping ground and neither ranks well. The export shows the problem and not where it comes from. The workaround people use is to filter the Performance view to one query in the interface and read which pages appear, one query at a time. Fine for three queries. Useless for eight hundred.

The API returns page and query in the same row. Among the rows Google returns, each page and query combination becomes a lookup instead of a manual filter.

It also gives you more rows. [The interface export stops at 1,000 representative rows](https://support.google.com/webmasters/answer/12919797), while an API request can return up to 25,000. And once one date range is a request, the previous range is just a second request.

## What that caught on my site

I had rewritten a post's title and meta description as a deliberate test, with a checkpoint four weeks out.

Paired data says 84% of the post's disclosed impressions are two variants of one string: somebody pasting a certification quiz question into Google. The keyword I was actually targeting has zero.

The test would have come back looking like a failed rewrite. It is not a failed rewrite. It has not started, because you cannot move a keyword the page does not rank for yet. If I had only read the page's click count, I would have recorded the wrong conclusion and trusted it for months.

## Setup

Fifteen minutes, all in the browser except the last step. You need a Google account with access to the property.

### 1. Pick or create a Cloud project

Go to [console.cloud.google.com](https://console.cloud.google.com) and use the project picker in the top bar. Any existing project works, so reuse one if you have it.

### 2. Enable the Search Console API

**APIs & Services**, then **Library**. Search for **Google Search Console API**, open it, click **Enable**.

Do not skip this because it sounds like a formality. If the API is off, your first request returns a 403, and a 403 reads as a permissions problem, so you will go back and check the user you added rather than the switch you never flipped. Enable it now and give it a minute to propagate.

### 3. Create a service account

**APIs & Services**, then **Credentials**, then **Create credentials**, then **Service account**. Give it a name.

Google will offer to grant it project roles. Skip that (it is optional, and the access that matters is granted in step 6). Click **Done**.

### 4. Create a JSON key

Click the service account, open the **Keys** tab, then **Add key**, **Create new key**, **JSON**.

It downloads immediately and it is the only copy. Google will not show it again.

### 5. Copy the service account address

Same page, looks like `something@your-project.iam.gserviceaccount.com`. It behaves like an email address even though nobody reads mail there.

### 6. Add it to your property

Over to [search.google.com/search-console](https://search.google.com/search-console). Pick the property, then **Settings**, **Users and permissions**, **Add user**.

Paste the address and give it **Restricted** access. [`searchAnalytics.query` needs read permission](https://developers.google.com/webmaster-tools/v1/prereqs), and Restricted is enough for the Performance data this fetcher reads. This is the step that grants access to your data. The Cloud side only created an identity.

### 7. Store the key out of the way

```bash
mkdir -p ~/.config/gsc && chmod 700 ~/.config/gsc
mv ~/Downloads/your-key.json ~/.config/gsc/example-com.json
chmod 600 ~/.config/gsc/example-com.json
```

The fetcher reads this working credential each time it asks Google for your data. Moving it here gives the API route a predictable, protected location. The manual export route does not use a key.

The commands do four things:

- `mkdir -p ~/.config/gsc` creates a configuration folder in your home directory.
- `chmod 700 ~/.config/gsc` prevents other normal local users from opening that folder.
- `mv` moves the downloaded key into that folder and renames it for the Search Console property.
- `chmod 600` prevents other normal local users from reading or changing the key.

The filename comes from the property name. For `sc-domain:example.com`, use `example-com.json`. When you give `search-visibility` that property name, its fetcher looks for this filename automatically. It reads the key locally to sign Google's request. You do not upload the file to Claude or paste its contents into a prompt.

Keep the file outside any web root and out of a project directory that gets deployed. One wrong rule and the key becomes downloadable. If these commands are unfamiliar, or you are using Windows, [use the AI helper below](#prompt). Give it your operating system and current setup, but never the key itself.

## Asking for page and query together

The request is a POST to:

```
https://www.googleapis.com/webmasters/v3/sites/{property}/searchAnalytics/query
```

The property goes in the path, URL-encoded, exactly as Search Console names it. A domain property is `sc-domain:example.com`, which encodes to `sc-domain%3Aexample.com`.

The body:

```json
{
  "startDate": "2026-06-01",
  "endDate": "2026-08-29",
  "dimensions": ["page", "query"],
  "rowLimit": 25000,
  "type": "web"
}
```

`dimensions` is the whole trick. Ask for `["query"]` and you get the table you already had. Ask for `["page", "query"]` and you get the pairing no export contains. [`rowLimit` maxes at 25,000 and `startRow` handles pagination](https://developers.google.com/webmaster-tools/v1/searchanalytics/query).

Pagination still does not mean complete data. Google says the API is limited internally and returns top rows rather than guaranteeing every row. It gives you a larger and more useful sample than the interface export, not a perfect copy of every search.

## Or skip the plumbing

I built this for my own site and packaged it, so you do not have to write the fetcher, the pagination, or the analysis.

```
/plugin marketplace add imisic/claude-marketplace
/plugin install search-visibility@imisic
```

Then `/search-visibility:a-seo-gsc`. [Claude Code namespaces skills installed through a plugin](https://code.claude.com/docs/en/discover-plugins), so the plugin name stays in the command. Give it your property name and it pulls the data, fetches a matching earlier window for comparison, and hands back a ranked action plan instead of a wall of rows. It finds pages earning impressions but no clicks, and queries sitting just off page one where a title rewrite beats a new post. It also finds where your own pages compete with each other, which is the one the pairing makes possible.

The bundled API fetcher needs Python 3 and `openssl` on macOS, Linux, or WSL. On native Windows, the skill offers WSL or the manual-export route before you create a key.

**You do not need the API to use it.** It opens by asking which route you are on. Say you would rather download the reports and it tells you exactly which three to export and reads those instead, because the fetcher writes Google's own export shapes and the parser cannot tell the two apart. The only thing the download route gives up is the page and query pairing, so that one section is skipped, not reported as clean. Every other part of the analysis runs identically.

The bundled parser does the arithmetic before Claude reasons over the findings, so the numbers are computed rather than guessed from a spreadsheet. The fetcher saves the raw exports locally and connects only to Google's OAuth and Search Console endpoints. Claude receives the parser's findings as part of the session, under the data controls of your Claude Code plan. The plugin adds no telemetry.

It is free, MIT, and the code is on [GitHub](https://github.com/imisic/claude-marketplace). The longer write-up is on the [Toolshed](/toolshed/plugins/search-visibility).

## Read this before trusting any query number

I checked the fetch against figures I already knew. The gap changed how I read every query number, and it applies to both routes.

[Google omits rare queries to protect privacy](https://support.google.com/webmasters/answer/96568) (reasonably enough). I compared one 28-day window on my own site two ways, page rows against query rows, both describing the same traffic.

The query rows saw 16% of the impressions and 8% of the clicks.

Everything I had concluded from a query table was a statement about a small disclosed minority, and I had been reading it as a statement about the site. Query data is still useful. It just needs that coverage ratio stated next to any finding, and you never subtract query clicks from page clicks and give the remainder a name.

I would expect the gap to narrow on a busier site, but I would check rather than assume. Once both tables arrive, it is one subtraction.

## What the API will not give you

[The Search Console API has no endpoint for the coverage report](https://developers.google.com/webmaster-tools/v1/api_reference_index). Indexed versus not indexed, the reason breakdown, the list of URLs sitting in "Crawled, currently not indexed", none of it. Still a manual export, and skipping it costs you the entire indexing half of any analysis.

The URL Inspection API looks like a substitute and is not. It reports on URLs you hand it, one at a time, so it can confirm pages you already know about and can never show you a page Google indexed that you never submitted. It cannot see index bloat, and index bloat is half of what the coverage report is for.

There is no API for the links report either.

So this replaces one of three exports, and it is the one you were doing most often.

## Worth it or not

If you check Search Console a few times a year and mostly want to know whether traffic is up, the export is fine and this is not worth your afternoon.

If you make decisions from it, run title tests, or have more than a few pages covering related ground, the pairing answers a question you have probably been guessing at. Fifteen minutes of clicking, then it runs on a schedule and you stop only looking when something has already gone wrong.

The reverse problem, getting data out of a system with no API at all, is a different kind of work. I wrote about [capturing email and calendar without a mailbox API](/blog/ai-tools/email-calendar-capture-without-api) when that was the constraint. Worth knowing which situation you are in before you start building.

Here the API existed the whole time. I just kept clicking Export.

## Sources

- [Search Analytics: query](https://developers.google.com/webmaster-tools/v1/searchanalytics/query), Google for Developers. Defines the endpoint, dimensions, 25,000-row request limit, pagination, and the top-row limitation.
- [Export data directly from a Search Console report](https://support.google.com/webmasters/answer/12919797), Google Search Console Help. Documents the 1,000-row export limit and representative sampling.
- [Prerequisites](https://developers.google.com/webmaster-tools/v1/prereqs), Google for Developers. Confirms that `searchAnalytics.query` requires read permission on the property.
- [API Reference](https://developers.google.com/webmaster-tools/v1/api_reference_index), Google for Developers. Lists the Search Console API services and confirms there are no coverage or links-report endpoints.
- [Discover and install prebuilt plugins through marketplaces](https://code.claude.com/docs/en/discover-plugins), Claude Code Docs. Documents marketplace installation and namespaced plugin skills.
