Skip to content
Notes

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.md inside the project folder.
  • Check that it loads: start a new session and ask Claude which skills it has. search-console-ga4 should 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.

One service account per identity→ the same email added to every site

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.

Rights are granted inside each product, not in Google Cloud. The roles follow Google's help pages for each product.

How to connect Search Console and GA4 to Claude Code

  1. 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.
  2. 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.
  3. Add the service account to each product. For Google it’s an ordinary user with an unusual email address.
  4. 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:

  1. Create the GA4 property and its web data stream. Take the measurement ID from the API’s answer — never type it.
  2. Create the Tag Manager container.
  3. Put the container snippet into the site template. Its ID comes from the site’s config, not hard-coded.
  4. Add the Google tag inside the container on the All Pages trigger, create a version, publish it.
  5. Check the live page.
  6. 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.

Four requests on August 4, 2026, minutes apart. The default returns only final data; rows by query leave out rare queries. Source: Search Console API, plants.place

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.

GotchaWhat I sawWhat to do
Final data4 impressions instead of 37 on a new siteSend dataState: "all" in every request
Query rows23 of 37 impressions had a visible queryTotals from site or page level; query rows as a guide
Sitemap count0 indexed while 7 of 8 sampled pages were in the indexCheck 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 collect request.
  • 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