Docs · 04 of 07
Connecting ad accounts
Set the reporting timezone first. It is the single setting most likely to make every CPA in the table wrong, and it is the least obvious when it is.
1. Set the reporting timezone
Marketing → Settings → Reporting timezone
Sessions are stored in UTC. Ad platforms report daily spend in the ad account’s own timezone. Joined naively, the wrong spend sits against the wrong day and the totals still look plausible. A campaign running in America/Los_Angeles reported against UTC days has up to eight hours of every day’s sessions filed against the previous day’s money.
Set this to the timezone your ad accounts run in. Once an account is connected, the settings page lists its timezone with a button to adopt it, so matching is a click rather than a transcription exercise.
2. Connect Google Ads
You need three things, and they come from two different places:
| Value | Where it comes from |
|---|---|
| Developer token | Your manager account, under API Center. A basic-access token is enough for daily campaign reporting. |
| OAuth2 refresh token | Generated once during connection, against a user with read access to the account. |
| Customer ID | The ten-digit number at the top right of the Google Ads UI, dashes optional. |
If the account sits under a manager account, the manager’s customer ID is also needed as the login customer ID. Without it the API rejects the request with a permissions error that does not mention MCC anywhere.
The sync runs a GAQL query against campaign, reading metrics.cost_micros, metrics.impressions, metrics.clicks and segments.date. Cost is divided by a million at the edge, so nothing downstream has to remember that Google reports money in micros.
3. Connect Meta
You need an ad account ID (the act_ number) and an access token.
Use a System User token. Regular user tokens expire, and they expire without warning at whatever moment is least convenient, taking the sync down until somebody notices the spend column has stopped moving. A System User token issued from Business Manager does not.
The sync calls /act_{id}/insights at level=campaign with time_increment=1. Ranges over thirty days go through async report jobs, because a synchronous call over months of data hits the rate limiter and takes the whole sync down with it.
4. Currency has to match
Every ad account on one site must report in the same currency. If they do not, the sync fails before any request goes out and tells you which account disagrees.
That is deliberate. Converting at a rate nobody checked produces a plausible number nobody questions, which is worse than a refused sync. Currency conversion is a later problem.
5. Pick your conversion events
Marketing → Settings → Conversion events
Choose from the observed event names rather than typing one. A name typed from memory that matches nothing produces a null CPA with no error anywhere.
Custom events covers how to send them in the first place.
6. Map campaigns to UTMs
Propose mappings creates rows automatically wherever exactly one observed UTM triple matches a campaign, by normalised name or by platform ID. Anything ambiguous or untagged is left alone for a person and shows up in the health panel.
A mapping you save by hand is never overwritten by a later sync. That is the whole reason mappings live in a table rather than being matched at query time: a mapping row survives a campaign rename, and a string comparison does not.
What the daily sync does
- Runs once a day and re-pulls a trailing seven-day window, overwriting what it finds. Both platforms restate recent days after the fact, so a same-day-only pull would permanently lock in whatever figure happened to be visible when the job fired.
- Makes a missed run self-healing. The next run restates the days the missed one would have written.
- Syncs accounts sequentially. Both platforms rate limit per developer token, and a burst across a manager account is the fastest way to get throttled.
- Requests campaign totals only. Nothing user-level is ever asked of an ad platform.
- Logs every run, its date range, and why it failed if it did. That log is on the settings page.
Where credentials live
Platform-wide credentials come from the environment. The optional per-account override, which exists because manager-account setups need one, is encrypted at rest with the instance secret so a database dump is not a set of live ad credentials.