Playbook · Analytics
Search Console and GA4 in Claude Code: one service account and the API gotchas
My AI agent reads Search Console and GA4 and sets up Tag Manager through one Google service account. Here is the setup, the permissions, and the API gotchas that once gave me wrong numbers — with a skill file to download.
On August 4 I asked my agent a question that sounded like an access problem. It had reported 4 impressions in Google for plants.place, a site that was a few days old. Search Console in my browser showed more. Did it need another service account?
It didn’t. The access was fine; the request wasn’t. The Search Console API returns only final data unless you ask for fresh data, and a site that young had almost no final data yet. With one more parameter, the same request returned 37 impressions.
That was one of several times an API answer and the real picture disagreed. This playbook is the setup I now use on every site, and the gotchas I hit along the way. The rules live in a skill file my agent loads whenever it touches analytics. Download it, or paste it into any agent’s instructions.
Install the skill in Claude Code
The file is a ready skill. Its header tells Claude Code what it is and when to load it:
---
name: search-console-ga4
description: Give an AI agent Search Console, GA4 and Tag Manager through one Google service account. Setup order, permissions, scripts, and the API gotchas (final vs fresh data, query rows vs totals, sitemap counts, URL Inspection limits). Load before setting up analytics on a site or pulling data from these APIs.
---
- For all your projects: save it as
~/.claude/skills/search-console-ga4/SKILL.md. - For one project: save it as
.claude/skills/search-console-ga4/SKILL.mdinside the project folder. - Check that it loads: start a new session and ask Claude which skills it has.
search-console-ga4should be on the list.
The file also holds two short Python scripts. One gets a token for the service account. The other creates a GA4 property and a Tag Manager container for a site. There is no Google SDK and no extra server: the agent calls Google’s APIs directly, and every change is a script you can read before it runs.
One service account per identity
Before this was a skill, I had to ask my agent how my Google service accounts were organised. Now the answer is written down once.
An identity, in my setup, is one set of accounts that belong together, like personal projects or one client. For Google it means one Analytics account and one Tag Manager account per identity, with a property and a container for each site inside them. And one Google Cloud project with one service account — per identity, not per site.
The reason is simple. The Cloud project only holds the switched-on APIs, the service account and the quota. Access to the data is granted inside each product. So a new site costs no work in Google Cloud: I add the same service-account email to its Search Console, GA4 and Tag Manager.
Search Console
Full user. Reads performance data, inspects URLs, submits sitemaps. Owner only if the agent has to add users.
GA4
Editor on the account. Creates properties and data streams. Viewer is enough to read reports.
Tag Manager
Administrator on the account. Creates containers. Publish on one container is enough to change and publish it.
How to connect Search Console and GA4 to Claude Code
- Create one Google Cloud project for the identity and switch on four APIs: Google Search Console API, Google Analytics Admin API, Google Analytics Data API and Tag Manager API.
- Create a service account and a JSON key. The key goes into a password manager, never into a chat. My agent reads it in code and never prints it. One trap: the private key inside the JSON has line breaks, and a password manager that stores one line can save it cut short. I store it base64-encoded.
- Add the service account to each product. For Google it’s an ordinary user with an unusual email address.
- Ask the agent for a dry run for one site, and read the plan before anything changes.
How to add a user in Search Console
Open Settings → Users and permissions → Add user. Paste the service account’s email and choose Full. That’s enough to read the data, inspect URLs and submit sitemaps. Only an owner can add other users.
In GA4 the place is Admin → Account access management. In Tag Manager it’s Admin → User Management. Give the roles from the figure above.
Setting up GA4 and Tag Manager through the API
The order is always the same:
- Create the GA4 property and its web data stream. Take the measurement ID from the API’s answer — never type it.
- Create the Tag Manager container.
- Put the container snippet into the site template. Its ID comes from the site’s config, not hard-coded.
- Add the Google tag inside the container on the All Pages trigger, create a version, publish it.
- Check the live page.
- Write the IDs into the project’s notes.
This site got its analytics that way on October 3. The dry run printed the plan and changed nothing:
[dry run] would create GA4 property mxkeey.com in accounts/…
[dry run] would create GTM container mxkeey.com
The first real run created the GA4 property. The very next request, for its data streams, came back with a 503: service unavailable. A minute or two later the same request worked, so the version in the skill retries server errors.
Then Tag Manager refused with a 404: “Not found or permission denied.” Nothing was missing. The service account was in the Tag Manager account, but not as an Administrator, and only an Administrator can create containers. I changed the role, and the next run did the rest:
GA4 property exists: properties/…
GA4 measurement ID: G-…
GTM container created: GTM-…
Google tag created and published, version 2
Then the check that counts. On the live page, the browser’s Network tab showed three requests: the container, the Google tag it loaded, and a collect request with en=page_view and the right measurement ID.
I learned to insist on that check on plants.place. There my agent typed the measurement ID into seven tags by hand and got it wrong. Every tag would have sent its data nowhere, and nothing would have looked broken. On the same day it told me the new events worked. They reached the page’s dataLayer, but the container had no triggers for them, so nothing went to GA4. Since then IDs come only from API answers, and “done” means a request on the live page.
The header of that project’s setup script says why it is a script at all: by hand, it’s twenty forms that aren’t written down anywhere. A script can be read again and run again.
Search Console API gotchas
Impressions the Search Console API returned for the same days
plants.place, first days in search. Default request: 4 impressions in total, 1 in rows by query. With dataState all: 37 in total, 23 in rows by query.
Final data by default. If you don’t send dataState, the API returns only final data. Google says final data is usually available after two to three days. The dashboard shows fresher numbers, so the two disagree. On a site a few days old the difference was 4 impressions against 37. On an older site it’s only the last two or three days — still enough to break a “last 7 days” report or a check the day after a release. Fresh data can still change a little before it becomes final, and that’s fine.
Rows by query don’t add up to the total. For the same days, the rows grouped by query added up to 23 of the 37 impressions. Google leaves rare, anonymised queries out of query-level data, and the totals also differ when you group by page instead of the whole site. Take totals from a request without the query dimension. Read query rows as a list of what people search, not as a sum.
The sitemap’s “indexed” count. In the first days the sitemap entry in the API said 61 URLs submitted, 0 indexed. URL Inspection, asked about a sample of pages at the same time, found 7 of 8 in the index. Later I found that the API reference marks that field as deprecated: don’t use it. Ask URL Inspection instead.
| Gotcha | What I saw | What to do |
|---|---|---|
| Final data | 4 impressions instead of 37 on a new site | Send dataState: "all" in every request |
| Query rows | 23 of 37 impressions had a visible query | Totals from site or page level; query rows as a guide |
| Sitemap count | 0 indexed while 7 of 8 sampled pages were in the index | Check indexing with URL Inspection |
Search Console API limits worth knowing
- URL Inspection API: 2,000 requests a day and 600 a minute per property. It shows the version in Google’s index only; there is no live test through the API. Inspect a sample, not the whole site every day.
- Search Analytics: up to 25,000 rows per request; page through with
startRow. Google’s guide says the API exposes at most 50,000 rows of data per day per search type. - Not in the API at all: “Request indexing” and crawl stats. The API has four parts: search analytics, sitemaps, sites and URL Inspection. That’s all.
All of this is from Google’s own API reference and its usage limits page, checked in October 2026. Limits change; the skill tells the agent to read the current page when a number matters.
GA4 and Tag Manager gotchas
- A brand-new property may answer 503 at first. Retry with a pause. Fail loudly on everything else.
- Tag Manager says “not found” when it means “no permission”. Check the role before you look for a typo.
- An event in the dataLayer is not an event in GA4. It needs a trigger, a tag and a published version. Check the
collectrequest. - Key events don’t look back. Google’s help says marking an event as key affects reports from that moment and doesn’t change historic data. For history, use the event count filtered by the event’s name.
- GA4 needs time. Realtime shows data in minutes, but processing can take 24–48 hours. Yesterday’s numbers this morning are not final.
What still needs a human
- Request indexing. A click in Search Console.
- Crawl stats. An export from the report in the browser.
- Verifying a Domain property. A DNS record you add yourself, before the service account can be added.
- Live search results. On one project the agent searched Google through a browser and hit a captcha after about 40 searches in a row. For live results I use a search results API instead.
On a client’s account
The same setup works for clients, with tighter rules:
- Read access by default.
- Changes in Tag Manager or GA4 only when I ask for them, as a script with a dry run.
- After publishing: check the events in Realtime from real visitors and save a copy of the container version.
- Ad accounts stay manual.
About the author
Max Kiriienko
Tech Lead SEO & Marketing from Ukraine. I design growth strategies and build the pipelines, tools and teams that execute them.
Read next