There's a weekly ritual every serious site owner knows: log in to Google Search Console, open the Performance report, export a zip file, unzip it, fight with the garbled filenames, and paste numbers into a spreadsheet. Then next week, do it again — and lose the thread of what changed since last time.
This guide replaces that ritual with a script. One command pulls your clicks, impressions, keyword rankings, top pages, and country breakdown into a readable terminal report — with an option to save the raw data for week-over-week comparisons. It's the exact setup we run on our own blog, and the script below is the one in production, unmodified except for the configuration values.
You'll need a Google account that owns a Search Console property, about ten minutes of browser setup, and basic Python. Let's go.
The Idea: A Spare Key for Your Script
The Search Console API won't accept your normal Google password — and it shouldn't. Instead, you create a service account: a machine identity that exists purely so scripts can act on your behalf. Think of it as a spare key you hand to your Python script, with exactly one permission: read your search data.
The setup has two halves, and people get them confused, so here's the map:
- Google Cloud side — create the service account and download its key
- Search Console side — add that service account to your property, like you'd add a colleague
Skip either half and you'll meet a 403 error. Do both and the API opens up.
Five-Step Setup (One Time, Ten Minutes)
Step 1 — Create a project. Open console.cloud.google.com, create a new project (call it whatever you like, e.g. my-blog-project). If it asks for a parent resource, choose No organization — that's the normal setting for personal accounts.
Step 2 — Enable the API. In the left menu, go to APIs & Services → Library, search for "Search Console API", and click Enable.
Step 3 — Create the service account. Go to IAM & Admin → Service Accounts → Create Service Account. Give it a name like gsc-reader. On the next screen you'll be offered project roles — skip it. The permission your script needs is granted inside Search Console, not here. The third screen (user access) can also be skipped.
Step 4 — Get the key. Open your new service account, go to the Keys tab, and click Add Key → Create new key → JSON. A file downloads — this is your script's spare key. It contains the service account email (client_email) you'll need in the next step, plus a private key.
Step 5 — Add the account to Search Console. Open search.google.com/search-console, select your property, and go to Settings → Users and permissions → Add user. Paste the service account email (it looks like gsc-reader@YOUR_PROJECT_ID.iam.gserviceaccount.com — replace YOUR_PROJECT_ID with your actual project ID) and grant Full access.
One security rule before we continue: treat the JSON key file like a password. Add it to your .gitignore before you ever run git add. A service account key can read your search data, and keys leaked on GitHub get picked up by bots within minutes.
The Script
#!/usr/bin/env python3
"""Pull Google Search Console data and print a readable report.
One-time setup: see the five steps in the accompanying article.
Requires: pip install google-api-python-client google-auth
Python 3.9+ recommended.
Usage:
python gsc_report.py # last 28 days
python gsc_report.py --days 7 # last 7 days
python gsc_report.py --save # also save raw JSON for comparisons
python gsc_report.py --json # machine-readable output
Config via environment variables (or edit the defaults below):
GSC_KEY_PATH — path to the service account JSON key
GSC_SITE_URL — your Search Console property URL
"""
import argparse
import json
import os
import sys
from datetime import datetime, timedelta
# ── Config ────────────────────────────────────────────────────────
KEY_PATH = os.environ.get("GSC_KEY_PATH", "gsc-key.json")
SITE_URL = os.environ.get("GSC_SITE_URL", "https://your-site.com/")
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
def build_service():
"""Authenticate with the service account and build the API client."""
from google.oauth2 import service_account
from googleapiclient.discovery import build
if not os.path.exists(KEY_PATH):
print(f"✗ Service account key not found: {KEY_PATH}")
print(" Complete the five-step setup first (see article).")
sys.exit(1)
creds = service_account.Credentials.from_service_account_file(
KEY_PATH, scopes=SCOPES)
return build("searchconsole", "v1", credentials=creds,
cache_discovery=False)
def query(service, start_date, end_date, dimensions=None, row_limit=100):
"""Run one searchanalytics.query request. Returns the raw response."""
body = {
"startDate": start_date,
"endDate": end_date,
"dimensions": dimensions or [],
"rowLimit": row_limit,
}
return service.searchanalytics().query(siteUrl=SITE_URL, body=body).execute()
def _bar(value, maximum, width=30):
"""Render a simple proportional bar."""
filled = int(round(value / maximum * width)) if maximum else 0
return "█" * filled
def main():
parser = argparse.ArgumentParser(description="Search Console report")
parser.add_argument("--days", type=int, default=28,
help="How many days back (default 28)")
parser.add_argument("--save", action="store_true",
help="Save raw JSON next to the script")
parser.add_argument("--json", action="store_true",
help="Print raw JSON only")
args = parser.parse_args()
end = datetime.now().date()
start = end - timedelta(days=args.days - 1)
start_s, end_s = start.isoformat(), end.isoformat()
service = build_service()
summary = query(service, start_s, end_s)
by_date = query(service, start_s, end_s, dimensions=["date"], row_limit=1000)
by_query = query(service, start_s, end_s, dimensions=["query"])
by_page = query(service, start_s, end_s, dimensions=["page"])
by_country = query(service, start_s, end_s, dimensions=["country"])
if args.json:
print(json.dumps({
"period": {"start": start_s, "end": end_s},
"summary": summary, "by_date": by_date, "by_query": by_query,
"by_page": by_page, "by_country": by_country,
}, indent=2))
return
if args.save:
out = f"gsc-{start_s}-to-{end_s}.json"
with open(out, "w") as f:
json.dump({
"period": {"start": start_s, "end": end_s},
"summary": summary, "by_date": by_date, "by_query": by_query,
"by_page": by_page, "by_country": by_country,
}, f, indent=2)
print(f"→ Saved raw data to {out}")
# The no-dimension query returns totals inside rows[0], not at the top level
totals = (summary.get("rows") or [{}])[0]
clicks = totals.get("clicks", 0)
impr = totals.get("impressions", 0)
ctr = totals.get("ctr", 0) * 100
pos = totals.get("position", 0)
print("┌─────────────────────────────────────────────┐")
print("│ Google Search Console — your site │")
print("├─────────────────────────────────────────────┤")
print(f" Period: {start_s} → {end_s} ({args.days} days)")
print(f" Clicks: {clicks} Impressions: {impr}")
print(f" CTR: {ctr:.2f}% Avg position: {pos:.1f}")
print("\n Daily impressions:")
rows = sorted(by_date.get("rows", []), key=lambda r: r["keys"][0])
maximum = max((r.get("impressions", 0) for r in rows), default=0) or 1
for row in rows:
imp = row.get("impressions", 0)
print(f" {row['keys'][0]}: {_bar(imp, maximum):30} {imp}")
print("\n Top Queries:")
for row in by_query.get("rows", [])[:15]:
q = row["keys"][0]
print(f" [{row.get('clicks', 0):>3} clicks | "
f"{row.get('impressions', 0):>4} impr | "
f"pos {row.get('position', 0):>5.1f}] {q}")
print("\n Top Pages:")
for row in by_page.get("rows", [])[:10]:
short = row["keys"][0].replace(SITE_URL, "")
print(f" [{row.get('clicks', 0):>3} clicks | "
f"{row.get('impressions', 0):>4} impr | "
f"pos {row.get('position', 0):>5.1f}] /{short}")
print("\n Countries:")
for row in by_country.get("rows", [])[:10]:
print(f" [{row.get('clicks', 0):>3} clicks | "
f"{row.get('impressions', 0):>4} impr] {row['keys'][0]}")
print("└─────────────────────────────────────────────┘")
if __name__ == "__main__":
main()
Install the two libraries, save the script as gsc_report.py, put your key file next to it as gsc-key.json, and run it:
pip install google-api-python-client google-auth
python3 gsc_report.py
What You Get
Here's the actual output from our own blog for a recent 14-day window:
┌─────────────────────────────────────────────┐
│ Google Search Console — your site │
├─────────────────────────────────────────────┤
Period: 2026-08-03 → 2026-08-16 (14 days)
Clicks: 5 Impressions: 984
CTR: 0.51% Avg position: 47.4
Daily impressions:
2026-08-04: 0
2026-08-05: 1
2026-08-09: █████ 56
2026-08-11: ███████████████ 174
2026-08-13: ██████████████████████████████ 337
...
The daily bars make trends obvious at a glance — in this case, a new site finding its way out of the search sandbox. The queries table below it shows exactly which keywords are climbing toward page one, which is how we decide what to write next.
The Pitfalls (All Hit in Real Life)
| Symptom | Cause | Fix |
|---|---|---|
403: "User does not have sufficient permission for site" |
Service account was never added to Search Console | Settings → Users → add the client_email |
403: "Search Console API has not been used in project" |
The API is not enabled | Enable it in Cloud Console → Library |
| Summary shows 0 clicks, 0 impressions | Totals live in rows[0], not the top level of the response |
Read (response.get("rows") or [{}])[0] |
Old tutorials show build("webmasters", "v3") |
The API was renamed | Use build("searchconsole", "v1") — same API, current name |
| GSC shows "add a property" instead of your site | You're logged into the wrong Google account | Check the avatar in the top-right corner |
pip install upgrades protobuf and breaks something |
Rare dependency collision | Accept the upgrade; if a tool breaks, pin protobuf in a venv |
One more honest note: Search Console data lags by roughly 2–3 days. Don't panic when yesterday's numbers look missing — that's normal, and the totals catch up on their own.
Going Further
The script has two features that turn a report into a system:
--savewrites the raw JSON with the date range in the filename. Run it weekly and you build a folder of snapshots — diff two files and you have your week-over-week ranking report, no spreadsheet involved.--jsonprints machine-readable output, ready to pipe into your own charts or dashboards.
And once the script is boring and reliable, schedule it with cron so the report is waiting in your terminal every Monday morning.
What's Next?
If you built your own site the way we did, you'll enjoy the under-$30 website guide that started our whole journey — Search Console is where you watch it grow. If automation is your thing, the Python news digest uses the same "script + schedule" pattern for a completely different job: your morning reading list, delivered by email.
The weekly export ritual is officially retired. Welcome to the two-second report.
All code in this article was tested and runs successfully on Python 3.8.10 (Ubuntu 20.04) with google-api-python-client 2.198.0 and google-auth 2.50.0 — verified against live Google Search Console data, August 2026.