Documentation
Integrations

Google Stack

Five Google APIs share one OAuth token. This page documents the agency-level pattern, scopes, and per-source quirks.

The agency-level token

Every Google integration uses the same OAuth token, stored as a single row in integrations with provider = 'google_master_agency'. The connecting account must have access to every client property we manage (we use the agency's shared Google account).

This avoids:

  • Per-client OAuth flows (cleaner client onboarding).
  • Token refresh per client (one place to maintain).
  • Permission churn when staff leave (we just rotate the agency account's password).

Scopes

The OAuth flow at /api/auth/google/route.ts requests:

ts
const SCOPES = [
  'https://www.googleapis.com/auth/userinfo.email',
  'https://www.googleapis.com/auth/analytics.readonly',         // GA4
  'https://www.googleapis.com/auth/webmasters.readonly',        // Search Console
  'https://www.googleapis.com/auth/business.manage',            // GBP Performance API
  'https://www.googleapis.com/auth/youtube.readonly',           // YouTube Data v3
  'https://www.googleapis.com/auth/adwords',                    // Google Ads
]
Scope changes invalidate tokens
Adding a new scope requires every Google integration to re-authenticate. Old tokens silently lack the new scope and the new endpoint returns 403. The most recent scope addition was adwords (Google Ads, 2026-04-03) — any integration not refreshed since then cannot pull ads data.

OAuth callback

/api/auth/google/callback exchanges the code for tokens and writes them to integrations:

ts
{
  provider: 'google_master_agency',
  access_token: '...',
  refresh_token: '...',
  scope: '...',
  is_active: true,
}

The refresh_token is critical. Google only returns it on the very first consent — if it's ever lost, the user must visit https://myaccount.google.com/permissions and remove the app, then reconnect.

Per-source notes

Google Analytics 4 live

  • Per-client field: clients.ga4_property_id.
  • API: analyticsdata.googleapis.com/v1beta/properties/{id}:runReport.
  • Route: /api/metrics/ga4 (and sub-routes for /traffic, /pages, /acquisition).
  • Auxiliary endpoint /api/google/properties lists every property the agency token can see — used by the admin client config form.

Search Console live

  • Per-client field: clients.gsc_site_url (must match exactly the property URL in GSC).
  • API: searchconsole.googleapis.com/v1/sites/{site}/searchAnalytics/query.
  • Routes: /api/metrics/gsc for aggregate (clicks/impressions/ctr/position), /api/metrics/gsc/keywords for the top 10 keyword list.

Google Business Profile live

  • Two clients exist: Performance API for actions/calls/website clicks, and Places API for rating/review count.
  • Per-client field: clients.gbp_location_id (Performance API), clients.gbp_place_id (Places API).
  • Performance API uses the OAuth token. Places API uses GOOGLE_API_KEY only.
  • Routes: /api/metrics/gbp, /api/metrics/gbp/reviews.
Two IDs, two APIs
The Performance API location_id and the Places API place_id are different identifiers for the same business. Both must be set on the client record to get the full GBP picture.

YouTube Data API v3 live

  • Per-client field: clients.youtube_channel_id. Accepts a channel ID (UCxxxx) or a handle (@brand) — the route resolves handles via search.
  • Endpoints: youtube.channels.list, youtube.search.list, youtube.videos.list.
  • Route: /api/metrics/youtube.

PageSpeed Insights live

  • Does not require OAuth. Uses GOOGLE_API_KEY.
  • Input is just clients.url.
  • Returns Mobile + Desktop scores. The dashboard renders them as circular gauges (SpeedGauge in ClientDashboard.tsx).
  • Route: /api/metrics/pagespeed.

Google Ads partial

  • Per-client field: clients.google_ads_customer_id (10-digit, no dashes).
  • Requires both the OAuth token and a developer token in GOOGLE_ADS_DEVELOPER_TOKEN + manager account in GOOGLE_ADS_LOGIN_CUSTOMER_ID.
  • API: Google Ads API v17 via the google-ads-api npm package (or raw fetch).
  • Route: /api/metrics/google-ads.
  • Status: integration code is in place but most clients have not been re-auth'd for the new scope. Audit on Denver before assuming any other client works.

Adding a new Google source

  1. Add the new scope to SCOPES in /api/auth/google/route.ts.
  2. Add the per-client column to clients via a new SQL patch file.
  3. Create the route under /api/metrics/<source> following the GA4 route as a template.
  4. Add the field to the admin edit form at /dashboard/clients/[id]/edit.
  5. Wire the fetch + render into ClientDashboard.tsx using the existing MetricCard pattern.
  6. Force every existing integration to re-authenticate (otherwise they will silently lack the new scope).