TLDR: GA4 (Google Analytics 4) hard-blocks service accounts from Workspace-org properties. Don't fight it — authenticate as a real user with a refresh token instead.

The Setup

I was building a webinar analytics dashboard for an ecommerce business.

One of the key metrics: "Clicks to Registration Page," pulled straight from GA4 in real time.

The documented GA4 Data API setup is simple: create a service account (a machine identity in Google Cloud), add its email to GA4's Property Access Management as a Viewer.

Should've taken ten minutes.

The Wall

GA4 threw a hard red error:

"Unable to add users — This email doesn't match a Google Account."

Even as a property admin. Refreshed. Tried again. Same thing.

My first instinct — of course — was to assume it was normal. A cosmetic warning. Just save it anyway.

Then I got pushed back at: "Web search this — don't guess."

The search took about thirty seconds. Real cause: the service account lives outside the ecommerce client's Google Workspace org, and the org policy hard-blocks adding external identities to GA4. It's not a misconfiguration. It's not a UI quirk.

It's a WALL.

GA4's own docs say "add a service account" like it's universal. It isn't. If your GCP project is external to the Workspace org that owns the GA4 property, you're blocked — full stop.

What Finally Worked

OAuth-as-user.

The insight is almost embarrassingly simple: a service account is an outsider to the org. A real Google user who already has GA4 access is an insider. So authenticate as the human.

Here's the one-time setup:

  1. Create a Web Application OAuth 2.0 client in GCP (a GA4 OAuth client, on the client's GCP project)
  2. Set the consent screen to Internal — durable refresh tokens, no verification banner, no 100-user cap, no scary click-throughs. Internal apps in a Workspace org just work.
  3. Mint a refresh token once via scripts/ga4-oauth-mint.mjs — a loopback (local) OAuth flow, no server needed
  4. Store it in your environment — the server uses the refresh-token grant (a type of OAuth flow that needs no redirect URI at runtime), so it works fine on Vercel (my deployment platform) without any callback routes

ga4.ts prefers OAuth via GA4_OAUTH_CLIENT_ID, GA4_OAUTH_SECRET, and GA4_OAUTH_REFRESH_TOKEN — with the old service account key as a silent fallback.

Then: node scripts/test-ga4.mjs

totalUsers(28d) = 58,597

LIVE.

Why This Matters to Me

I almost accepted the wrong explanation and moved on.

The correct move — always — is to search the actual error string on an unfamiliar platform before asserting what it means. Third-party platforms have environmental constraints that don't show up in the main docs. GA4's service-account block is one of them.

If your GCP service account is external to the GA4 property's Workspace org: don't burn time debugging the grant. Go straight to OAuth-as-user, set Internal consent, mint once, store the refresh token.

The setup guide won't tell you this. Now you don't have to learn it the hard way.

P.S. If your consent screen is shared with other OAuth clients (like sign-in for a web app), double-check those clients have no external users before flipping to Internal — it restricts all of them to org-domain users only.