Browse documentation

Connecting Google sources

Two paths cover GA4 (traffic) and GTM (tag audit plus optional snippet publish). Google Ads is OAuth only.

  1. Service account — an agent with Admin on the client’s GA4/GTM can finish the grant with no human click. Otherwise the human pastes one email into GA4/GTM Admin. No consent screen, no FunnelKeeper dashboard.
  2. OAuth — one Google sign-in covers GA4, GTM, and Google Ads. The refresh token is stored encrypted. The agent hands auth_url to the human; they only ever see Google’s consent page.

GTM asks for tagmanager.readonly plus tagmanager.edit.containers and tagmanager.publish so Integrations can deploy the event tracking snippet on an explicit confirm. OAuth grants minted before those write scopes existed are still read-only — reconnect Google to upgrade. A service-account grant with Publish on the container has the write scopes from the start.

Login with Google (the dashboard Sign in / Create account button) uses the same OAuth client and a different callback: /auth/google/callback. That flow asks only for openid email profile and never stores a refresh token. The client must list both redirect URIs — login and this connect grant. Provision it with ./scripts/setup-google-oauth.sh.

From the dashboard

Integrations → Connect with Google → consent → pick your property/container → done.

Or Grant access instead on the GA4 / GTM card: copy the service-account email, add it as Viewer (GA4) or a container user (GTM), then Verify. No redirect, no consent screen.

From a terminal or agent — service account (preferred)

POST /connect/google/service-account/start {"product_slug":"demo-product","kinds":["ga4","gtm"]}
  → {"sa_email":"fk-…@….iam.gserviceaccount.com","instructions":{…}}

instructions.ga4.agent and instructions.gtm.agent are the exact Google API calls. If you have Admin, make them; otherwise give sa_email to the human.

POST /connect/google/service-account/verify {"product_slug":"demo-product"}
  → {"status":"complete","options":{"ga4_properties":[…],"gtm_containers":[…]}}

POST /connect/google/service-account/verify {"product_slug":"demo-product","ga4_property_id":"4210…","gtm_container_path":"accounts/…/containers/…"}
  → {"ok":true,"activated":["ga4","gtm"]}

MCP: connect_service_account then verify_service_account. CLI: fk connect google-sa --product demo-product.

From a terminal or agent — OAuth fallback

The consent screen can’t be skipped or scripted — Google requires a human. The flow hands the URL out; give it to the human directly (chat or open). They never enter the FunnelKeeper dashboard. Then poll:

POST /connect/google/start {"product_slug":"demo-product","kinds":["ga4","gtm"],"client":"cli"}
  → {"state":"…","auth_url":"https://accounts.google.com/o/oauth2/v2/auth?…","expires_at":"…"}

Give auth_url to the human. Then poll (bearer required; the state is bound to your account and expires after 10 minutes):

GET /connect/google/status?state=…
  → {"status":"pending"}                          keep polling (2s, then 5s)
  → {"status":"complete","results":[…],"options":{"ga4_properties":[…],"gtm_containers":[…]}}
  → {"status":"error","error":"access_denied"}

When options holds more than one property or container, ask the human which, then:

POST /connect/google/select {"state":"…","ga4_property_id":"4210…","gtm_container_path":"accounts/…/containers/…"}
  → {"ok":true,"activated":["ga4","gtm"]}

Exactly one option? Pass it straight through — no need to ask.

What GTM is for

Nothing is ever written to your container. The Keeper reads the live version weekly and answers one question: is the GA4 tag this product expects present and unpaused? If spend is running while the answer is no, you get an alert card — because every funnel number under-counts until the tag is fixed.

Pick the Ads account the same way you pick a GA4 property. FunnelKeeper reads campaign spend for the last 7 days each night — cost, impressions and clicks, in the account’s own currency — and never writes to your account. If your accounts sit under a manager (MCC), the manager is resolved for you; there’s nothing extra to enter.

Spend sync switches on automatically once FunnelKeeper’s Google Ads developer token is approved. Until then your account id is stored and the connection waits in pending, and the CSV bridge covers spend imports. Nothing fails in the meantime — Test on the Integrations page tells you which state you’re in.