Ads Platforms
Google Ads and Meta Ads — both pulled live, both with per-client OAuth quirks.
Google Ads partial
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 aslogin-customer-idheader.clients.google_ads_customer_id— the actual ad account ID for that client.
Route
GET /api/metrics/google-ads?customer_id=XXX&start_date=YYYY-MM-DD&end_date=YYYY-MM-DDReturns 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_idvia the admin edit form. - If the agency token was issued before 2026-04-03, reconnect Google so the
adwordsscope is included. - Visit
/api/test/google-adsto verify Denver still works, then check the new client's dashboard.
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(theact_xxxxID). - Per-client integration row:
integrations.provider = 'meta_ads',client_idset,access_tokenstored. - 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
GET /api/metrics/meta-ads?account_id=act_XXX&start_date=YYYY-MM-DD&end_date=YYYY-MM-DDCalls 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_IDandMETA_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:
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.