Documentation
Integrations

Ads Platforms

Google Ads and Meta Ads — both pulled live, both with per-client OAuth quirks.

Google Ads piggybacks on the agency-level Google OAuth token. See the Google Stack page for OAuth details. The wrinkle: Google Ads requires three things on top of OAuth.

  • GOOGLE_ADS_DEVELOPER_TOKEN — issued by Google to our agency. One token per company.
  • GOOGLE_ADS_LOGIN_CUSTOMER_ID — the manager (MCC) account ID. We pass it on every request as login-customer-id header.
  • clients.google_ads_customer_id — the actual ad account ID for that client.

Route

text
GET /api/metrics/google-ads?customer_id=XXX&start_date=YYYY-MM-DD&end_date=YYYY-MM-DD

Returns a normalised payload with adSpend, clicks, impressions, conversions, avgCpc, ctr, costPerConversion, each as { value, change, formatted }.

Onboarding a new client to Google Ads

  • Verify the client's Google Ads account is linked to our manager account (request access via Tools → Account access).
  • Get the 10-digit customer_id (no dashes).
  • Save it to clients.google_ads_customer_id via the admin edit form.
  • If the agency token was issued before 2026-04-03, reconnect Google so the adwords scope is included.
  • Visit /api/test/google-ads to verify Denver still works, then check the new client's dashboard.
403 / unauthenticated
99% of Google Ads errors are scope-related. If a single client errors, check whether the agency token has the adwords scope (look at integrations.scope). If yes but it still fails, check that the client account is actually linked to our MCC.

Meta / Facebook Ads partial

Meta does not have a clean agency model like Google Ads. Each client requires its own OAuth flow and stores tokens per-client.

  • Per-client field: clients.meta_ad_account_id (the act_xxxx ID).
  • Per-client integration row: integrations.provider = 'meta_ads', client_id set, access_token stored.
  • Long-lived token: we exchange the short-lived OAuth token for a 60-day token at callback time.
  • Refresh: when a token nears expiry, we re-exchange it. If it has already expired, the client must reconnect.

Route

text
GET /api/metrics/meta-ads?account_id=act_XXX&start_date=YYYY-MM-DD&end_date=YYYY-MM-DD

Calls Meta Marketing API /insights endpoint. Returns spend, impressions, clicks, CTR, CPC, CPM, leads, cost-per-lead, landing page views, and link clicks.

Meta App Setup

  • META_APP_ID and META_APP_SECRET — set up our Meta Developer App in Business mode.
  • Required permissions: ads_read, read_insights, business_management, ads_management.
  • The app must be in Live mode and approved for the above permissions before non-admin users can connect.

ROAS calculation

ROAS is computed in the dashboard, not pulled from any single platform. The formula in PipelineSection.tsx is:

ts
const adSpend = (googleAdsData?.adSpend.value ?? 0) + (metaAdsData?.adSpend?.value ?? 0)
const revenue = ghlMetrics?.pipelineValue.value ?? 0
const roas = adSpend > 0 ? ((revenue / adSpend) * 100).toFixed(1) + '%' : '-'

Revenue is sourced from GHL (sum of won deal values). Spend is the sum of Google Ads + Meta Ads spend over the same date range. There is also an external roas key that some clients push from n8n if their revenue lives outside GHL.