Technical SEO Mastery · Migrations, auditing workflow and monitoring · lesson 18 of 18 · 16 min
Search Console API and BigQuery: automating performance and indexing data
Why automate Search Console
The Search Console interface is great for exploration but limited for scale: the Performance report shows up to 1,000 rows in the UI, you can't join it with revenue or crawl data, and you can't alert on it. The Search Console API and the bulk data export to BigQuery fix that. This lesson builds three things: an API pull for query-page data, a bulk URL inspection sample (Lesson 1.1 showed the basics), and BigQuery SQL for brand/non-brand and page-group reporting.
Access and authentication
- Create a Google Cloud project, enable the Google Search Console API, and create a service account. Add the service account's email as a user on the Search Console property (restricted access is enough for reading).
- Store the key file path in an environment variable — never commit it. For personal scripts, OAuth user credentials also work.
- Scope:
https://www.googleapis.com/auth/webmasters.readonly.
Hands-on 1: Search Analytics API with pagination
# pip install google-api-python-client google-auth pandas
import os, pandas as pd
from google.oauth2 import service_account
from googleapiclient.discovery import build
creds = service_account.Credentials.from_service_account_file(
os.environ["GSC_KEY_FILE"], scopes=["https://www.googleapis.com/auth/webmasters.readonly"])
gsc = build("searchconsole", "v1", credentials=creds, cache_discovery=False)
def query_all(site, start, end, dims=("query", "page"), search_type="web", row_limit=25000):
rows, start_row = [], 0
while True:
body = {"startDate": start, "endDate": end, "dimensions": list(dims),
"type": search_type, "rowLimit": row_limit, "startRow": start_row, "dataState": "final"}
resp = gsc.searchanalytics().query(siteUrl=site, body=body).execute()
batch = resp.get("rows", [])
rows += batch
if len(batch) < row_limit:
break
start_row += row_limit
df = pd.DataFrame([{**dict(zip(dims, r["keys"])), "clicks": r["clicks"], "impressions": r["impressions"],
"ctr": r["ctr"], "position": r["position"]} for r in rows])
return df
df = query_all("sc-domain:example.com", "2026-08-01", "2026-08-28")
df["segment"] = df.page.str.extract(r"^https://www\.example\.com/([^/]+)/")[0].fillna("home")
df["brand"] = df["query"].str.contains(r"example ?brand|examplebrand", case=False, regex=True)
print(df.groupby(["segment", "brand"])[["clicks", "impressions"]].sum().sort_values("clicks", ascending=False))
Notes: the API returns at most the rows Google stores for the property and omits anonymised queries; use dataState: "all" if you need the freshest (incomplete) days; respect quota limits and back off on 429 errors.
Hands-on 2: BigQuery bulk export
In Search Console → Settings → Bulk data export, link a Google Cloud project. Google then exports daily tables to a BigQuery dataset, including searchdata_site_impression (property-level aggregation) and searchdata_url_impression (URL-level). Exports start from the day you enable them — no backfill — so turn it on early.
-- Non-brand clicks and impressions by page group, last 28 complete days (URL-level table)
SELECT
REGEXP_EXTRACT(url, r'^https://www\.example\.com/([^/]+)/') AS page_group,
SUM(clicks) AS clicks,
SUM(impressions) AS impressions,
SAFE_DIVIDE(SUM(clicks), SUM(impressions)) AS ctr,
SAFE_DIVIDE(SUM(sum_position), SUM(impressions)) + 1 AS avg_position
FROM `my-project.searchconsole.searchdata_url_impression`
WHERE data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY) AND DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
AND search_type = 'WEB'
AND NOT is_anonymized_query
AND NOT REGEXP_CONTAINS(LOWER(query), r'example ?brand')
GROUP BY page_group
ORDER BY clicks DESC;
Position fields in the export are zero-based sums, hence + 1 after dividing by impressions. Check the export schema documentation for current field names before building dashboards. Connect the dataset to Looker Studio (or another BI tool) for page-group dashboards with release annotations.
URL Inspection at scale: sample, don't sweep
The URL Inspection API (Lesson 1.1) has per-property daily and per-minute quotas, so it can't inspect a large site exhaustively. Use it as a sampling instrument: for each page group, inspect a random sample (for example 50-100 URLs) monthly, store verdict, coverage state and Google-selected canonical in BigQuery next to the performance data, and chart the share of each group that is indexed with a matching canonical. For whole-site coverage, rely on the Page indexing report filtered by sitemap segment.
Hands-on 3: an alert on page-group clicks
Schedule a query that compares the last 7 complete days with the previous 7 by page group, and alert when a group falls more than a threshold you choose (for example 30%) with enough volume to matter. Pair it with the change monitor from Lesson 7.2: most sudden drops coincide with a template, robots or redirect change.
Worked example: a UK publisher's brand/non-brand truth
A UK recipe publisher (illustrative) reports "organic up 10%" in the UI, but the BigQuery split shows branded clicks up (a TV mention) and non-brand clicks flat, with the "quick answer" page group down. The team uses the data to prioritise deeper content on the declining group and to report honestly to leadership, annotating the TV appearance.
Measuring success
- Reports refresh automatically; no one copies data from the UI.
- Brand/non-brand and page-group trends are available within minutes.
- Alerts fire within days of material drops, and each is traced to a cause.
Common mistakes
- Summing average position across rows (always weight by impressions).
- Forgetting anonymised queries when reconciling totals.
- Enabling bulk export too late — there is no historical backfill.
- Committing service account keys to a repository.
Video lecture: Search Console API and BigQuery: automating performance and indexing data
Lecture coming soon · 14 chapters · about 8 minutes. Read the full transcript below.
- Search Console API + BigQuery
- Access
- Hands-on 1: Search Analytics API
- Gotchas
- Hands-on 2: bulk export to BigQuery
- The SQL
- Hands-on 3: the drop alert
- Example 1: the Monday email
- Example 2: brand vs non-brand truth (illustrative)
- Measures and mistakes
- Indexing data at scale
- Watch me do it: zero to page-group table
- Governance
- Recap and try this now
Lecture transcript
Search Console API + BigQuery
Search Console's interface is brilliant for exploring. It's terrible for scale. It shows a limited number of rows, you can't join it with revenue or crawl data, and it won't tell you on a Monday morning that a page group dropped thirty percent last week. In this lecture you'll automate Search Console with its API and the BigQuery bulk export, split brand from non-brand properly, and set up an alert that catches drops within days.
Access
First, access. Create a Google Cloud project, enable the Search Console API, and create a service account. Then add that service account's email address as a user on your Search Console property. Restricted access is enough for reading. Keep the key file path in an environment variable and never commit the key to a repository. The scope you need is webmasters read-only. For personal scripts, OAuth user credentials work too.
Hands-on 1: Search Analytics API
Hands-on one: the Search Analytics API. The function in the lesson text calls search analytics query with a date range, dimensions like query and page, the web search type, a row limit of twenty-five thousand, and a start row. It keeps paging until a batch comes back smaller than the limit. Then it builds a pandas data frame. From there, the script extracts a page group from the URL, flags branded queries with a regex, and sums clicks and impressions by group and brand.
Gotchas
A few details that trip people up. The API omits anonymised queries, so query-level totals won't match page totals. Recent days are incomplete; use data state final for stable reporting, or all if you need the freshest partial data. Respect quotas and back off when you get four-twenty-nine errors. And never average the average position column across rows. Weight it by impressions, or better, use the BigQuery export, which gives you sums.
Hands-on 2: bulk export to BigQuery
Hands-on two: the bulk data export. In Search Console settings, open Bulk data export and link a Google Cloud project. Google then exports daily tables into BigQuery, including a site-level impressions table and a URL-level impressions table. There's one crucial detail: exports start from the day you enable them. There's no backfill. So switch it on early, even if you won't use it for months.
The SQL
The SQL in the lesson text computes non-brand clicks, impressions, CTR and average position by page group for the last twenty-eight complete days. It extracts the page group with a regular expression, filters to web search, excludes anonymised queries and branded terms, and calculates average position as the sum of position divided by impressions, plus one, because the export stores zero-based position sums. Connect the dataset to Looker Studio or another BI tool for dashboards with release annotations.
Hands-on 3: the drop alert
Hands-on three: an alert. Schedule a query that compares the last seven complete days with the previous seven, by page group. Alert when a group falls by more than a threshold you choose, say thirty percent, and only when volume is large enough to matter. Pair it with the change monitor from the previous lesson, because most sudden drops coincide with a template, robots or redirect change. The alert tells you where; the change monitor usually tells you why.
Example 1: the Monday email
Worked example one, simple. A Leeds accountancy firm's marketing manager spends two hours every month copying Search Console data into a spreadsheet. A thirty-line script using the API now pulls the same data, groups it by service page, and emails a table every Monday. It saves time, but more importantly, the numbers are consistent every month, which the manual process never was.
Example 2: brand vs non-brand truth (illustrative)
Worked example two, with illustrative details. A UK recipe publisher reports organic up ten percent in the interface. The BigQuery split tells a different story. Branded clicks are up, thanks to a TV mention, while non-brand clicks are flat, and the quick-answer page group is down. The team uses the data to prioritise deeper content for the declining group, and reports honestly to leadership, with the TV appearance annotated on the chart.
Measures and mistakes
How do you measure success? Reports refresh automatically, and nobody copies data out of the interface. Brand versus non-brand and page-group trends are available in minutes. And alerts fire within days of material drops, with each one traced to a cause. Common mistakes: summing or averaging average position without weighting, forgetting anonymised queries when reconciling totals, enabling bulk export too late, and committing service account keys to a repository.
Indexing data at scale
What about indexing data? The URL Inspection API has daily and per-minute quotas per property, so you can't inspect a large site exhaustively. Treat it as a sampling instrument. Each month, inspect a random sample of fifty to a hundred URLs per page group, store the verdict, coverage state and Google-selected canonical in BigQuery next to your performance data, and chart the share of each group that's indexed with a matching canonical. For whole-site coverage, use the Page indexing report filtered by sitemap segment.
Watch me do it: zero to page-group table
Watch me do it. I'll get from zero to a non-brand page-group table. Step one: in Google Cloud, I create a project, enable the Search Console API, create a service account, and download its key into a folder outside the repository. I set the GSC key file environment variable to that path. Step two: in Search Console, Users and permissions, I add the service account's email with restricted access. Step three: I run the Search Analytics script for the last twenty-eight days with query and page dimensions. It pages through three batches of twenty-five thousand rows. Step four: I check the output against the interface: page-level clicks for the top page match; query totals are lower, as expected, because anonymised queries are omitted. Step five: in Settings, I turn on bulk data export to the same project. Nothing appears today; tables start tomorrow, with no backfill. Step six: a few days later, I run the SQL from the lesson in BigQuery and get non-brand clicks, CTR and weighted average position by page group. Step seven: I connect it to Looker Studio and add annotations for releases. Now leadership has one official view.
Governance
And a word on governance. Once Search Console data lives in BigQuery, it becomes company data. Decide who can access the dataset, how long you keep it, and which dashboards are the official source of truth, so marketing, product and leadership stop arguing over different numbers. Document the brand regex and page group rules in one place, and version them, because changing a regex silently changes every trend. Small discipline, big payoff in trust.
Recap and try this now
Recap. Use the API for flexible pulls with pagination, and the bulk export for complete daily data in BigQuery. Split brand and non-brand, group by page type, weight position properly, and alert on real drops. Try this now. Turn on bulk data export today, even if you won't use it yet. Then run the API script for the last twenty-eight days, and look at non-brand clicks by page group. That's the view your leadership actually needs.
Key takeaways
- The API and BigQuery bulk export overcome the UI's row limits and let you join, segment and alert.
- Authenticate with a service account added as a Search Console user; keep keys out of code and repositories.
- Page through Search Analytics results with startRow; anonymised queries are omitted; weight position by impressions.
- Bulk export has no backfill — enable it early; compute average position as Σ sum_position / Σ impressions + 1.
- Alert on page-group drops and pair alerts with change monitoring to find causes quickly.
Try it
Enable bulk data export, run the Search Analytics script for 28 days, and produce a non-brand clicks table by page group for your stakeholders.