BYPASSING THE MATRIX: FIXING SERVICE ACCOUNT API BUGS IN GOOGLE ANALYTICS & SEARCH CONSOLE

If you have ever tried to connect a custom backend, a local automation script, or a homelab Model Context Protocol (MCP) server to Google Analytics 4 (GA4) or Google Search Console using a Google Cloud Service Account, you know it should take five minutes.

Instead, you usually run into a wall of undocumented API errors, frontend validation blocks, and architectural disconnects.

Here is a post-mortem of the exact failures you might encounter while trying to link a service account ([email protected]) to a property (example.com), and the hidden backdoors required to bypass them.

Who Is This Guide For?

This guide is for developers and engineers who are building custom tooling that needs programmatic access to Google Analytics and Search Console data. You might be setting up an MCP server, building a custom SEO dashboard, automating indexing requests, or connecting analytics data to an internal pipeline. If you are using a Google Cloud Service Account and hitting permission walls, this is the guide you need.

By the End of This, You’ll Know…

  • Why both GA4 and GSC UIs reject valid service account emails — and how to bypass each
  • How URL encoding traps cause mysterious 400 errors with domain properties
  • Why “split-brain” architecture means ownership ≠ access, and the exact API call to fix it
  • The complete workflow to get a single service account fully operational with both GA4 and GSC

Part 1: The Google Analytics 4 (GA4) UI Bug

The Problem

You create a secure Service Account in the Google Cloud Console. You go to your GA4 Dashboard, navigate to Admin → Property Access Management → Add User, paste the service account email, and hit save.

Instantly, the UI throws a red error claiming: “This email doesn’t match a Google Account.” This happens even if your Workspace domain allowlist is perfectly configured.

The Root Cause

The engineers who maintain the GA4 web interface recently pushed an overly aggressive front-end validation script. The web form incorrectly applies consumer email rules (checking to see if it’s a standard registered Gmail or Workspace user) and automatically blocks developer service accounts before the request ever reaches the actual database.

This is not a backend issue. The validation happens entirely in the browser before the request ever reaches Google’s servers. The backend will accept the service account — the UI just refuses to let you try.

The Fix: Bypass the UI with the Admin API

Because the backend server will accept the service account, you just have to bypass the broken web form.

  1. Get your GA4 Property ID (the number after the p in your dashboard URL).
  2. Go to the GA4 Admin API Explorer for accessBindings.create .
  3. Set the parent field to properties/YOUR_PROPERTY_ID.
  4. In the Request Body, inject the service account directly:
{
  "user": "[email protected]",
  "roles": ["predefinedRoles/viewer"]
}
  1. Click Execute and authenticate with your admin account. You will get a 200 OK, and the service account will bypass the UI block and gain instant access.

Key insight. The accessBindings.create endpoint goes directly to the backend IAM store, skipping the broken client-side validator entirely. The “Viewer” role is sufficient for querying reports via the Data API — you don’t need Editor or Admin.


Part 2: The Google Search Console Cascade

Search Console has the exact same UI validation bug as GA4, but bypassing it introduces two additional API quirks. That means three separate bugs stand between you and a working GSC service account.

Bug 1: The “Not a Google Account” UI Block

The Problem

You go to the modern Search Console UI (Settings → Users and Permissions → Add User), paste your service account email ([email protected]), and click add.

Google throws the same validation error as GA4: the email address does not match a valid Google Account.

The Root Cause

Exactly the same broken pattern as GA4 — client-side validation that expects standard Google Account formats (@gmail.com or Workspace domains) and rejects IAM service account identities before the request reaches the server.

The Fix: Bypass the UI with the Site Verification API

Unlike GA4 (which has accessBindings.create), Search Console requires you to use the Site Verification API to grant ownership.

The workflow:

  1. Enable the Google Site Verification API in your GCP project
  2. Do not try to add the service account through the Search Console UI — use the API directly
  3. The service account now has verified ownership, even though the UI never acknowledged it
# Get an access token for the service account
ACCESS_TOKEN=$(gcloud auth print-access-token \
  --impersonate-service-account=[email protected])

# Verify ownership via Site Verification API
curl -X POST \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  "https://www.googleapis.com/site-verification/v1/webResource/insert?verificationMethod=DNS" \
  -d '{
    "site": {
      "type": "INET_DOMAIN",
      "identifier": "example.com"
    }
  }'

Key insight. The Site Verification API and the Search Console UI use separate validation pipelines. The API validates against the IAM identity store; the UI validates against a hardcoded pattern matcher.

Bug 2: The API Explorer URL Double-Encoding Trap (400 Bad Request)

The Problem

When pulling your verified site list via the API, Google returns unique identifiers for your Domain Properties. For domain properties, the ID looks like this: dns%3A%2F%2Fexample.com.

However, when you paste that exact string into the API Explorer’s id field to update your permissions, the server rejects it with a 400 Bad Request: “The site example.com of type INET_DOMAIN is invalid.”

The Root Cause

The API Explorer web form automatically URL-encodes whatever you type into its text parameters. Since the ID was already encoded (%3A and %2F), the form double-encodes it behind the scenes (converting it to something like dns%253A%252F...). The Google backend tries to read this garbled string and fails to parse the structure.

This is a classic encoding-on-encoding trap. The list_sites response gives you pre-encoded IDs, but the API Explorer treats all input as raw text and encodes it again.

The Fix

Always strip out the encoding and pass the raw protocol string into the id field:

  • Change: dns%3A%2F%2Fexample.com
  • To: dns://example.com

If you are writing code instead of using the Explorer, this is less of an issue because HTTP libraries handle encoding consistently. But if you are debugging with the Explorer, this trap will waste hours.

Pro tip. When working with the API Explorer, always test with curl first to confirm your parameters are correct.

Bug 3: The Split-Brain Registry (403 Forbidden)

The Problem

You successfully patch the API, getting a beautiful 200 OK confirming your service account is an official owner of dns://example.com. Yet, when your backend application or MCP tool tries to fetch data using list_sites, it returns an empty object ({}). If it tries to force-query the domain directly, it crashes with a 403 Forbidden: User does not have sufficient permission.

The Root Cause

Google Search Console operates on a “split-brain” architecture:

  1. The Core Ownership Registry: Verifies who legally owns a domain.
  2. The User Profile Registry: Tracks which verified properties are actively pinned to an individual user or service account’s dashboard.

Just because a service account is granted ownership does not mean the property is automatically activated on its personal profile layout. Because its profile is blank, it doesn’t recognize its own rights to the data.

This is the most frustrating bug because the 200 OK from the ownership verification makes you think everything is working. The failure only appears when you try to actually use the data.

The Fix

You must force the service account to manually register the property into its personal profile index. This can be accomplished by executing an explicit sites.add API call authenticated as the service account:

curl -X PUT \
  -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" \
  -H "Content-Type: application/json" \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com"

Once this profile update runs, the list_sites array instantly populates, the 403 authorization block vanishes, and your backend tool can pull indexing data flawlessly.

Important. The sites.add call must be authenticated as the service account itself, not as your personal Google account. This is what registers the property on the service account’s profile — your personal account already has the property, but the service account doesn’t.


The Complete Workflow: One Service Account for GA4 + GSC

Here is the end-to-end workflow to get a single service account fully operational with both Google Analytics and Google Search Console:

flowchart TD A[Create Service Account in GCP] --> B{GA4 UI accepts SA?} B -->|No| C[Use Admin API accessBindings.create] B -->|Yes - rare| D[Add via GA4 UI] C --> E[GA4 access granted] D --> E A --> F{GSC UI accepts SA?} F -->|No| G[Use Site Verification API] F -->|Yes - rare| H[Add via GSC UI] G --> I{list_sites returns data?} H --> I I -->|Empty| J[Call sites.add as service account] I -->|Data returns| K[Done - both GA4 and GSC ready] J --> K E --> L[GA4 ready] L --> K

Validation Steps

# 1. Verify GA4 access
curl -X POST \
  -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" \
  -H "Content-Type: application/json" \
  "https://analyticsdata.googleapis.com/v1beta/properties/G-YOUR_PROPERTY_ID:runReport" \
  -d '{
    "dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}],
    "metrics": [{"name": "activeUsers"}]
  }' | jq .

# 2. Verify GSC access — list sites
curl -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" \
  "https://www.googleapis.com/webmasters/v3/sites" | jq .

# 3. Query GSC search analytics
curl -X POST \
  -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" \
  -H "Content-Type: application/json" \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -d '{
    "startDate": "2026-05-01",
    "endDate": "2026-05-31",
    "dimensions": ["query"],
    "rowLimit": 10
  }' | jq .

If all three return successful responses, your service account is fully operational with both GA4 and Search Console.


Architectural Takeaways

When dealing with Google’s cloud ecosystem, remember that the API and the Web UI are not equal mirrors of each other.

  • Web UIs have strict frontend validation layers that sometimes ignore edge cases like service accounts. The UI is a convenience layer, not the source of truth.
  • Legacy systems frequently throw backend errors when processing new changes before they propagate completely. The split-brain architecture means ownership and profile activation are separate operations.
  • When automated tools break down due to formatting assumptions (like blindly trying to prefix https:// onto domain properties), dropping down to explicit, raw HTTP API patches will always get you across the finish line.

What You Can Actually Use Today

  • GA4 Admin API — Use accessBindings.create to add service accounts without the broken UI.
  • Google Site Verification API — The reliable way to add service account ownership. Avoid the Search Console UI for service accounts entirely.
  • Google Search Console API — The sites.add endpoint is the key to activating properties on service account profiles. Use sc-domain:example.com format for domain properties.
  • Google Cloud IAM — Use gcloud auth print-access-token --impersonate-service-account to get tokens for testing without managing JSON key files.

Technical resources: Google’s GA4 Admin API documentation, Search Console API documentation, and Site Verification API reference provide additional context on authentication and authorization flows.

For a broader look at GCP security misconfigurations, see our guide on GKE Security Exploits /.