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/propertieslists 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/gscfor aggregate (clicks/impressions/ctr/position),/api/metrics/gsc/keywordsfor 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_KEYonly. - 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 (
SpeedGaugeinClientDashboard.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 inGOOGLE_ADS_LOGIN_CUSTOMER_ID. - API: Google Ads API v17 via the
google-ads-apinpm 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
- Add the new scope to
SCOPESin/api/auth/google/route.ts. - Add the per-client column to
clientsvia a new SQL patch file. - Create the route under
/api/metrics/<source>following the GA4 route as a template. - Add the field to the admin edit form at
/dashboard/clients/[id]/edit. - Wire the fetch + render into
ClientDashboard.tsxusing the existingMetricCardpattern. - Force every existing integration to re-authenticate (otherwise they will silently lack the new scope).