I Wired Google Search Console Into My Publishing Pipeline and Immediately Hit Someone Else's Google Account
I'd been checking my blog's search traffic by hand -- log into Search Console, click the property, read two numbers, close the tab -- often enough that wiring it into a script felt like an obvious next step. What I didn't expect was to spend the first ten minutes of that "obvious next step" locked out of my own property by a Google account I'd forgotten I was signed into.
Why I wanted this as a script instead of a habit
Everything else in this project already reports on itself through small Python scripts -- how many posts are live, how many views each one has on dev.to. Search Console was the one piece still living entirely in a browser tab, which meant it was also the one piece I sometimes forgot to check. Wrapping it in the same pattern as the others -- run a script, read three numbers -- was less about saving clicks and more about making it something I'd actually keep doing.
The Search Console API needs its own OAuth scope (webmasters.readonly) and its own token file, separate from the one Blogger already uses, but otherwise reuses the same client secret and the same installed-app-flow pattern I'd already built. I expected this to be the fast, boring part of the afternoon.
The first run
The browser consent screen popped up like it always does. I had two Google accounts signed in on that machine -- a main one and a very similarly named second one I'd created ages ago for something unrelated -- and picked one without thinking too hard about which. The script ran, called sites().list() to confirm which properties it could see, and came back empty. Querying the actual site URL directly returned a 403: User does not have sufficient permission.
My first assumption was a scope problem -- maybe webmasters.readonly wasn't actually enough, or maybe I'd fat-fingered the site URL format (Search Console is picky about the trailing slash and about domain-property vs URL-prefix-property matching). Neither was it. The account that had just clicked "Allow" simply wasn't the account that owned the property.
Finding out which account actually owned it
The two email addresses differ by a single character -- one has a trailing digit the other doesn't -- which is exactly the kind of near-miss that's invisible when you're clicking through a consent screen on autopilot. To sort it out, I went to Blogger directly and opened the small account-switcher in the corner, the one that lists every Google account currently signed in on that browser. One of the two was marked "Default," and that was the one actually attached to the blog. Re-running the OAuth flow and deliberately picking that account fixed the 403 immediately -- and, on the very next attempt, I picked wrong again out of habit and had to redo it a second time before it stuck.
Nothing about the API error mentioned accounts at all -- a 403 for insufficient permission looks identical whether the real problem is a missing scope, a missing role, or, like here, an entirely different human identity behind the token. Worth checking before anything else if a Google API call fails with a permissions error on a property you're certain you own: which account actually clicked "Allow."
A second, smaller bug once the account was right
With the right account authenticated, the summary query still failed, this time with a much more specific complaint: HttpError 400: '28daysAgo' is not a valid date string. Please use YYYY-MM-DD format. I'd copied the date-range pattern straight from how I already query Analytics, where relative strings like "28daysAgo" or "today" are first-class citizens. The Search Console API doesn't support that convenience at all -- it wants literal ISO dates, computed by the caller.
The fix was a couple of lines with datetime:
from datetime import date, timedelta
END_DATE = date.today().isoformat()
START_DATE = (date.today() - timedelta(days=28)).isoformat()
summary = service.searchanalytics().query(
siteUrl=SITE_URL,
body={"startDate": START_DATE, "endDate": END_DATE, "dimensions": []},
).execute()
Small fix, but it's the kind of thing that's easy to assume by analogy from a sibling API and only discover is wrong when the error message spells it out.
What it actually reports
With both bugs out of the way, the script runs cleanly and prints the same numbers I'd have gotten clicking through the UI: clicks, impressions, average CTR, and average position over the trailing 28 days, plus optional breakdowns by page and by query when there's enough data to break down. Right now those numbers are still close to zero -- the site is new enough that Google hasn't sent it meaningful search traffic yet -- so the script isn't reporting good news so much as it's reporting the same, real, unflattering number without me having to remember to go look for it.
The actual lesson
None of this was a hard bug in the sense of needing deep debugging -- both fixes were small once identified. The time sink was entirely in misdiagnosing a permissions error as a technical one when it was really a "which human is this token pretending to be" problem. Any time an OAuth-based script throws a 403 on a resource you're sure you have access to, checking which account actually granted consent is now the first thing I do, before touching scopes, roles, or API documentation at all.
Comments
Post a Comment