Skip to content
Back to Resources
Guide

The Search Console API: Query Data Without the UI

Skopx Team
August 21, 2026
17 min read

The Search Console API lets you pull the same clicks, impressions, CTR, and position data you see in the Performance report, but as JSON you can filter, join, and store yourself. The single endpoint that matters is searchanalytics.query, a POST request that takes a date range, a list of dimensions, optional filters, and a row limit, and returns up to 25,000 rows per call.

That one endpoint replaces most of what people do manually in the interface: exporting a CSV, pasting it into a spreadsheet, and re-doing the whole thing next month. Once you can call it programmatically, you can compare periods without re-exporting, track thousands of queries instead of the top 1,000 the UI hands you, and keep a history that outlives the 16-month retention window Google enforces on its own storage.

This guide covers what the API returns, how dimensions and filters actually behave, why property type changes your results, how much data lag to expect, and where the sampling and privacy filters quietly remove rows you assumed were there.

What Does the searchAnalytics.query Endpoint Actually Return?

The endpoint is POST https://searchconsole.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query. The siteUrl path segment must be URL-encoded, which trips up almost everyone on their first attempt. A domain property is written as sc-domain:example.com and encodes to sc-domain%3Aexample.com. A URL-prefix property is written as https://example.com/ and encodes to https%3A%2F%2Fexample.com%2F, trailing slash included.

The request body is small. A minimal query looks like this:

{
  "startDate": "2026-07-01",
  "endDate": "2026-07-31",
  "dimensions": ["query", "page"],
  "rowLimit": 25000,
  "startRow": 0
}

The response is a rows array. Each row has a keys array whose order matches the dimensions array you sent, plus four metrics: clicks, impressions, ctr, and position. CTR is a decimal fraction, not a percentage, so 0.0412 means 4.12 percent. Position is an average across impressions, weighted by impression count, and it is the metric people misread most often.

There is no cursor or page token. Pagination is offset-based through startRow. You request 25,000 rows, and if you get exactly 25,000 back you request the next 25,000 starting at row 25,000, and you keep going until a response comes back short. A response with fewer rows than your limit means you have reached the end of the result set for that query shape.

The other endpoints in the API are smaller in scope but useful. sites.list enumerates the properties the authenticated account can read, which is how you avoid hardcoding property strings. urlInspection.index.inspect returns the indexing state, canonical, and last crawl date for a single URL, rate-limited to a couple thousand calls per day per property. sitemaps.list and sitemaps.get report submitted sitemaps and their error counts. Everything else you might want, including the index coverage report and the Core Web Vitals report, is not exposed through this API at all.

Which Dimensions Can You Combine, and What Happens When You Do?

There are six dimensions: query, page, country, device, searchAppearance, and date. You can combine them freely with one exception, and understanding how combination works is the difference between a report that reconciles and one that does not.

DimensionValues returnedCommon useGotcha
queryThe search term, lowercasedKeyword tracking, intent groupingHeavily filtered for privacy; totals will not match
pageCanonical URL as Google chose itPer-page performanceNot the URL the user clicked if a canonical differs
countryISO-3166-1 alpha-3 code (usa, gbr)Market splitsThree letters, not the two-letter codes you expect
deviceDESKTOP, MOBILE, TABLETMobile experience checksTablet volume is often too small to act on
dateYYYY-MM-DD in Pacific TimeTrend lines, daily deltasNot your local timezone, and not UTC
searchAppearanceRich result types, AMP, video, etc.Feature eligibility auditsCannot be combined with other dimensions

That last row is the exception. searchAppearance has to be queried alone. If you want appearance data split by page, you run one query to get the appearance types present, then run a filtered query per appearance type with page as the dimension. It is two round trips instead of one, and it is the standard workaround.

The critical mental model: every dimension you add multiplies the row count and shrinks the per-row values. A query for ["date"] over 30 days returns 30 rows whose clicks sum to your true total. The same range with ["date", "query", "page", "country", "device"] returns tens of thousands of rows whose clicks sum to noticeably less. That is not a bug. Google removes rows tied to rare queries to protect user privacy, and the finer you slice, the more rows fall below that threshold and disappear.

The practical consequence: never derive site totals from a query-dimensioned pull. Run a separate low-cardinality query for totals and a high-cardinality query for detail, and accept that they will not reconcile. If you present both numbers in the same report, label them, because someone will notice the gap and assume the pipeline is broken.

How Do Filters Work in searchAnalytics.query?

Filters live in dimensionFilterGroups, an array of groups. Each group has a groupType and a filters array. In practice groupType is always and, because or is not supported at the group level. Filters within a group are ANDed together. To express OR logic you run separate queries and merge the results client side.

{
  "startDate": "2026-07-01",
  "endDate": "2026-07-31",
  "dimensions": ["query"],
  "dimensionFilterGroups": [{
    "groupType": "and",
    "filters": [
      { "dimension": "page", "operator": "contains", "expression": "/resources/" },
      { "dimension": "country", "operator": "equals", "expression": "usa" },
      { "dimension": "query", "operator": "notContains", "expression": "brandname" }
    ]
  }],
  "rowLimit": 25000
}

The operators are equals, notEquals, contains, notContains, includingRegex, and excludingRegex. The regex operators use RE2 syntax, which is fast but deliberately limited: no backreferences, no lookahead, no lookbehind. Regex matching is case-insensitive by default and capped at 4,096 characters of pattern.

Two filter behaviors surprise people regularly. First, you can filter on a dimension you did not request. Filtering by page while grouping by query is completely legal and is the single most useful pattern in the whole API: it answers "what queries bring people to this specific page" without pulling the full page-by-query cross product.

Second, filtering does not restore filtered-out rows. If a query was suppressed for privacy in the unfiltered pull, narrowing the filter will not surface it. The suppression happens before your filter is applied, not after.

A third parameter worth knowing is dataState. The default is final, which excludes the most recent partial days. Setting it to all includes fresh but incomplete data, which is useful for a daily monitoring job and misleading for a month-over-month report. Pick one per use case and stay consistent, because switching between them mid-analysis produces phantom trends.

Why Does Property Type Change Your Numbers?

Search Console has two property types and they do not measure the same thing. A URL-prefix property covers exactly one protocol and subdomain combination: https://www.example.com/ does not include https://example.com/, http:// variants, or blog.example.com. A domain property covers every subdomain and every protocol under the registered domain, verified through DNS.

If you are running the same API query against both and getting different numbers, that is expected. The domain property is a superset. The differences show up in predictable places:

  • Subdomains. A domain property folds blog., docs., app., and any staging subdomain that leaked into the index into one dataset. If your docs subdomain earns significant impressions, a URL-prefix property on the www host will simply not see them.
  • Protocol drift. Old http:// URLs that still receive impressions appear only in a domain property.
  • m. mobile hosts. Legacy separate-mobile setups split across properties unless you use a domain property.

For programmatic reporting, the practical advice is to use a domain property as your source of truth and apply page filters to carve out the segments you care about. That way one API call shape serves every segment, and adding a subdomain later does not require verifying a new property and backfilling.

One caveat: if you inherited a URL-prefix property with years of history and just created the domain property, the domain property's history begins when Google starts associating data with it, and you may not get a full 16 months immediately. Run both in parallel for a while before you retire the old one.

If you are auditing which properties even exist and whether they are configured sensibly, that check belongs in a broader technical review. Our website audit checklist covers property setup alongside the other structural items that tend to rot quietly, and technical SEO automation covers how to schedule those checks instead of remembering to run them.

How Much Lag Should You Expect, and How Should You Handle It?

Search Console data is not real time. In normal operation, data for a given day becomes reasonably complete two to three days later. The API will happily return data for yesterday if you set dataState: "all", but that data is partial and will grow.

This creates a specific class of bug in reporting pipelines. A dashboard that pulls the last 7 days every morning will show the most recent days rising each time it refreshes, which reads as a growth trend when it is just backfill arriving. Someone screenshots Monday's chart, compares it to Wednesday's, and concludes something changed.

Three habits prevent that:

  1. Set an explicit end date offset. For any report meant to be stable, end the range three days before today. Yesterday's number is a forecast, not a fact.
  2. Re-fetch a trailing window. If you are storing rows in your own database, re-pull the last 5 to 7 days on every run and upsert, rather than only appending new days. Backfill will overwrite the partial rows with complete ones.
  3. Store the fetch timestamp. When a number changes between two runs, you want to know whether the data changed or your code did.

The other clock that matters is the 16-month retention limit. Search Console keeps roughly 16 months of data and then drops the oldest. If you want year-over-year comparisons that survive past that window, you have to store the rows yourself. This is the single strongest argument for building an API pipeline at all: not convenience, but the fact that data you do not save is data you permanently lose.

Timezone is the third clock. The date dimension uses Pacific Time. If your business reports in another timezone, daily boundaries will not line up with your other analytics sources, and the discrepancy is largest for a site with evening traffic peaks. Do not try to correct for it by shifting dates. Note the timezone in the report and move on.

What Are the Quotas and How Do You Stay Inside Them?

The API enforces limits at two levels: per-site and per-user, measured in queries per minute and per day. In practice a well-behaved reporting job never approaches them, and a poorly written one hits them within seconds by firing every page's query in parallel with no concurrency control.

Practical guardrails that keep pipelines healthy:

  • Serialize by property. Run one property's pulls at a time rather than fanning out across every property you manage simultaneously.
  • Cap concurrency at a small number. Two to four in-flight requests is plenty. Rate limit errors are far more expensive than the seconds you save.
  • Implement exponential backoff on 429 and 503. Retry after 1, 2, 4, 8 seconds with jitter. Do not retry 400-level errors other than 429; a malformed request will stay malformed.
  • Cache aggressively. Yesterday's finalized data will not change. If you have already fetched a complete day, do not fetch it again except during your trailing re-fetch window.
  • Prefer one wide query to many narrow ones. Pulling ["date", "page"] once and aggregating locally beats a per-page loop by an enormous margin, both in latency and quota.

Authentication is OAuth 2.0 or a service account. For a personal script, OAuth with a refresh token is simplest. For anything scheduled and unattended, use a service account and add its email address as a user on the Search Console property, exactly as you would add a human collaborator. Full access is not required for reading; restricted access is enough for searchanalytics.query, though it does limit some other endpoints.

One authentication detail that costs people an afternoon: adding the service account to a Google Analytics property does nothing for Search Console. They are separate permission systems on separate properties, and the error you get for missing permission looks identical to the error you get for a mistyped property string.

What Should You Build Once You Have the Data?

Having the rows is not the point. Here are the analyses that are painful in the UI and straightforward once the data is in a table you control.

Query-to-page mapping drift. Group by ["query", "page"] for two periods and find queries whose top-ranking page changed. When Google switches which of your pages it ranks for a term, position and CTR usually suffer while the two pages compete. This is invisible in the UI unless you go looking query by query.

Striking distance with impression weight. Filter to rows with average position between 8 and 20 and impressions above a threshold you set. Sort by impressions. These are terms where a modest ranking improvement produces real click volume, as opposed to position-3-to-2 movements on terms nobody searches.

CTR against expected CTR by position. Compute your own median CTR per position bucket from your own data, then flag pages performing well below their bucket. Low CTR at a good position usually means the title and description do not match the query intent. Generic curves from published studies are worse than your own numbers, because your industry, your brand recognition, and the SERP features on your terms all shift the baseline.

Cannibalization detection. For each query, count distinct pages receiving impressions. Queries where three or more of your pages all get meaningful impressions are candidates for consolidation.

Content decay. Compare rolling 28-day windows per page across several months. Pages declining steadily while the site grows are usually stale rather than penalized, and they are the cheapest wins available because the URL already has history.

None of these require machine learning or a large budget. They require the data in a form you can run a GROUP BY against, which is exactly what the Search Console API gives you.

How Does This Fit With the Rest of Your Measurement Stack?

Search Console tells you what happened in Google's blue-link results. It does not tell you how fast your pages load for real users, whether your structured markup is valid, or whether AI assistants mention your brand when someone asks a buying question. Each of those needs a different source.

For performance, the PageSpeed Insights API returns both lab Lighthouse scores and, where enough traffic exists, real-user Core Web Vitals from the Chrome UX Report. Our PageSpeed Insights API guide covers the request shape and the lab-versus-field distinction, and Core Web Vitals monitoring covers what to do when the two disagree.

For the emerging AI-answer layer, Search Console is silent by design. If someone asks an assistant for a recommendation in your category and a competitor is named instead, no Search Console row records that. Measuring it requires running buyer-intent prompts through search-grounded AI and checking who gets cited. Our guides on AI visibility tracking and generative engine optimization cover the method, and AI citation tracking covers the measurement mechanics specifically.

For the scoring layer that sits on top of all this, SEO health score explained walks through how to weight signals from different sources into a number that actually moves when you fix something.

Where Does Skopx Fit?

Skopx is an AI work platform that connects nearly 1,000 business tools, and Search Console is one of them. The Site Health feature pulls Lighthouse scores through the Google PageSpeed Insights API, real-user Core Web Vitals through CrUX, Search Console performance data, and an in-house on-page SEO audit that produces a 0-100 score with a specific fix list. The point is that you get the same data this article describes without maintaining the OAuth flow, the pagination loop, the backoff logic, and the storage schema yourself.

Beyond reporting, you can build the follow-up work as chat-built workflow automations: pull the query data on a schedule, flag pages whose position dropped past a threshold, and route them somewhere your team will see. The AI Visibility feature covers the layer Search Console cannot see, generating buyer-intent prompts from your site, running them through search-grounded AI, and reporting share of voice plus the citation gaps where competitors get named instead of you. It also tracks competitor pulse through sitemap and pricing-page diffs, and surfaces community openings in live Reddit and Hacker News threads.

Skopx runs on your own key with zero markup, or on the AI allowance included with a plan. Solo is $5 per month and Team is $16 per seat per month. Skopx operates with SOC 2 controls in place.

Frequently Asked Questions

How far back does the Search Console API go?

Roughly 16 months. Google retains about 16 months of Search Console data and drops the oldest as new data arrives, so a request for a start date beyond that window returns no rows rather than an error. If you want longer history, the only reliable option is to pull the rows through the API and store them in your own database. Once a day passes out of the retention window it is gone from Google's side permanently, so start storing before you need the history rather than after.

Why do my API totals not match the Search Console UI?

Almost always because of dimension cardinality or dataState. The UI's summary numbers come from a low-cardinality aggregate, while an API pull dimensioned by query loses rows to privacy filtering, so the sums legitimately differ. Check three things in order: whether you requested the same date range including both endpoints, whether dataState is final in one place and all in the other, and whether you are comparing a query-dimensioned pull against a site total. Also confirm the property string matches, since a URL-prefix property and a domain property on the same site are different datasets.

Can I get every keyword my site ranks for from the API?

No. The API returns queries that met Google's privacy threshold, and long-tail terms with very few searches are filtered out entirely. You will typically see many more rows than the UI's 1,000-row export limit, often a large multiple of it, but the removed rows are not recoverable by any parameter, filter, or authentication level. The gap between your query-level click sum and your site-level click sum is a rough measure of how much is hidden, and on sites with heavy long-tail traffic that gap can be substantial.

Do I need a service account or is OAuth enough?

Both work for reading data, and the choice depends on whether a human is present. Use OAuth with a stored refresh token for interactive scripts you run yourself. Use a service account for anything scheduled, because OAuth refresh tokens tied to a personal account break when that person leaves or changes their password. With a service account, add its email address as a user on the Search Console property directly. Restricted access is sufficient for searchanalytics.query.

Is there an official client library?

Google publishes client libraries for Python, Node.js, Java, Go, PHP, and several other languages, and all of them handle the OAuth token refresh and request signing for you. That said, the API surface is small enough that a plain HTTPS request with a bearer token works fine, and many people skip the library entirely to avoid the dependency. If you do use raw requests, the only real work is the token refresh, which is one POST to Google's OAuth endpoint.

How do I query a domain property versus a URL-prefix property?

The difference is entirely in the siteUrl path segment. A domain property uses the form sc-domain:example.com, URL-encoded as sc-domain%3Aexample.com. A URL-prefix property uses the full origin with a trailing slash, https://example.com/, encoded as https%3A%2F%2Fexample.com%2F. Everything else about the request is identical. If you are unsure which properties your credentials can reach, call sites.list first and use the exact strings it returns rather than constructing them by hand.

A Working Approach

If you are starting from nothing, build it in this order. First, get a single successful searchanalytics.query call with ["date"] as the only dimension and verify the click total roughly matches the UI. That proves authentication and property encoding are right, which is where most failures live.

Second, add pagination and pull ["date", "page", "query"] for a full month, writing rows to a table with a composite key on those three fields. Third, add the trailing re-fetch so the last week gets upserted rather than appended. Fourth, and only then, write the analyses. The temptation is to start with the interesting query and work backward, and it usually produces a pipeline that silently drops rows.

The Search Console API is a small, stable, well-documented surface. The complexity is not in the endpoint, it is in understanding that the numbers are aggregated, filtered, delayed, and scoped to a property in ways that all have to be accounted for before the analysis means anything. Once you internalize that, everything downstream gets easier.

Share this article

Skopx Team

The Skopx engineering and product team

Related Articles

Stay Updated

Get the latest insights on AI-powered code intelligence delivered to your inbox.