Blogger API 403 "The caller does not have permission": Check Which Account Your Token Belongs To
I publish to two Blogger blogs from Python: this one, and a Traditional Chinese sister blog. One day every write to the Chinese blog started failing. Creating a page, updating an old post: everything came back with the same response:
403 {
"error": {
"code": 403,
"message": "The caller does not have permission",
"errors": [
{
"message": "The caller does not have permission",
"domain": "global",
"reason": "forbidden"
}
],
"status": "PERMISSION_DENIED"
}
}
The token was valid. Reading the blog worked. Writing to this English blog with the same token still worked. Only writes to the other blog failed. Here's how I tracked it down, and the checks I now run first whenever Blogger says "The caller does not have permission".
What the 403 actually means
A 403 from the Blogger API is not an authentication error. If the token were expired or revoked, you'd get a 401 or an invalid_grant when refreshing it. A 403 means Google knows exactly who you are, and that account isn't allowed to do this to this blog.
So the question isn't "is my token broken?" but "whose token is this, and what role does that account have on the blog?"
Step 1: ask the token who it is
The API can tell you which account a token belongs to, which blogs that account is a member of, and whether it has admin rights on a specific blog. This uses the small no-dependency client from my Blogger API post:
# Who does this token belong to, and which blogs can it write to?
# `api` and `API` come from the small client in my Blogger API post:
# API = "https://www.googleapis.com/blogger/v3"
# api(method, url, body=None) -> parsed JSON, raises on HTTP errors
BLOG_ID = "your-blog-id"
me = api("GET", f"{API}/users/self")
print("token user:", me.get("displayName"), me.get("id"))
for b in api("GET", f"{API}/users/self/blogs").get("items", []):
print("member of:", b["name"], b["url"])
info = api("GET", f"{API}/users/self/blogs/{BLOG_ID}?maxPosts=0")
print(info.get("blog_user_info")) # look at hasAdminAccess
My output (IDs shortened):
token user: v 1191...
member of: VIC Maker Lab https://vicmakerlab.blogspot.com/
member of: (a second blog) https://...
{'kind': 'blogger#blogPerUserInfo', 'userId': '1191...', 'blogId': '3082...', 'photosAlbumKey': '0', 'hasAdminAccess': False}
That answered it immediately. The token belonged to the Google account that owns this English blog. The Chinese blog wasn't in its list of blogs at all, and hasAdminAccess was False.
How the token ended up on the wrong account
A few days earlier I had re-authorized the app (moving the OAuth consent screen from "Testing" to "In production", so refresh tokens would stop expiring every 7 days). On the Google sign-in screen I picked the account that owns the English blog. The new token was perfectly valid, just for a different person than before.
Nothing in the token file tells you which account you signed in with. If you have more than one Google account in your browser, it's easy to pick the wrong one on the consent screen and not notice until a write fails days later.
The fix: give that account a role on the blog
You have two options:
- Add the token's account to the blog (what I did). In the Blogger dashboard of the blog that's failing, go to Settings → Permissions → Blog admins and authors, invite the account, accept the invite from that account, then set its role to Admin. One token now works for both blogs.
- Re-authorize with the other account. This fixes the failing blog but can break the one that was working, if that account isn't a member there.
After the invite was accepted and the role set to Admin, the exact same script created all three pages on the first try. No code change, no new token.
Admin vs. author
Blogger has two roles for team members. Authors write and manage their own posts. Admins can also manage pages, layout, settings and other people's posts. If your script creates pages (About, Privacy Policy, Contact) or edits posts someone else wrote, give the account the Admin role. My failing account wasn't a member at all, so I only tested the "not a member" case; I gave it Admin rather than test whether Author would be enough.
When you create a page, the URL comes from the first title, so I create it with an English title and rename it afterwards:
# Creating a Blogger page (About, Privacy Policy, ...) needs admin rights on the blog.
# Tip: the page URL comes from the FIRST title, so create it with an English
# title to get /p/about.html, then rename it.
page = api("POST", f"{API}/blogs/{BLOG_ID}/pages?isDraft=false",
{"title": "About", "content": html})
api("PATCH", f"{API}/blogs/{BLOG_ID}/pages/{page['id']}", {"title": "關於我"})
print(page["url"]) # https://<blog>.blogspot.com/p/about.html
One thing the API can't do: search descriptions
While I was fixing up both blogs, I also tried to set each post's search description (the meta description Google may show in results) from the API. The post resource has a customMetaData field, so I sent a JSON description in it. The PATCH returned 200, the field didn't come back in the response, and the published page had no meta description. As far as I can tell, per-post search descriptions can only be set in the Blogger web editor, after you turn on Settings → Meta tags → Enable search description.
Checklist for "The caller does not have permission"
- Run the three "who am I" calls above. Is the blog in the token's blog list?
- Check
hasAdminAccessfor that blog. Pages and other people's posts need Admin. - If you recently re-authorized, assume you may have signed in with a different Google account.
- Fix it in Settings → Permissions on the blog, not in your code.
- A 401 or
invalid_grantis a different problem: an expired or revoked token. See the 7-day token note in my Blogger API post.
Comments
Post a Comment