Back to Blog
How-ToConnectorsMarketing Data

Connect Google Ads to Dashfeed in an Afternoon

The Google Ads API returns no row at all for a day with zero impressions, and it restates conversions for weeks after the fact. Connecting the account takes minutes. Getting spend and conversion metrics that stay right takes knowing those two facts.

Dashfeed Research8 min readAugust 20, 2026

What you’ll have by the end

A Google Ads account syncing daily into your warehouse, a year of campaign history backfilled, a gap-filled daily spend model you can trust, and monitors on it that fire for the right reasons. You need admin access to a Dashfeedworkspace and a Google login that can see the ad account. No developer token, no API keys, nothing to paste.

Part one. Connect the account

  1. Open Connections and add a source. Pick Google Ads from the catalog. It’s under Marketing next to Meta Ads and Google Local Services Ads.
  2. Authorize with Google. The wizard sends you to Google’s consent screen. Sign in with an account that has access to the ad account, not necessarily the one that pays for it. When you come back, the step reads “Authorized” and you continue.
  3. Pick the client account. Ad platforms only reveal which accounts you can see after you’ve authorized, so this step appears after the sign-in. Choose the client account you want to sync. If you go through a manager account, the wizard handles the manager relationship for you. If you try to sync the manager account itself, the connection test fails with a message telling you to select a client account, because manager accounts have no performance data of their own.
  4. Choose streams. The defaults are campaigns, ad groups, ads, and daily campaign performance. Advanced streams add ad group, ad, keyword, search term, and geographic performance. Start with the defaults. You can add the rest later without re-authorizing.
  5. Leave the lookbacks alone, for now. Two settings matter. The conversion lookback, thirty days by default, is how far behind the cursor every sync re-reads. The initial lookback, twelve months by default, is how much history the first sync pulls. Both have a reason to exist, covered below.
  6. Pick a destination and review. Tables land in your warehouse, or the managed one, under a schema named after the connection. If you called it “my google ads” you’ll get my_google_ads.campaigns and my_google_ads.campaign_performance. Create the connection and the first sync starts.

The first sync walks the initial lookback in monthly windows, so a year of history is twelve requests per report stream rather than one enormous one. If a window fails, it’s recorded as a gap and retried next run, and the cursor only advances past windows that succeeded. You’ll see the tables fill in over the next little while.

What lands, exactly

TableSync modeKeyNotes
campaignsFull refreshcampaign_idEvery campaign, including ones that never served
ad_groupsFull refreshad_group_idEntity table, no metrics
adsFull refreshad_group_id, ad_idEntity table, no metrics
campaign_performanceIncremental by datecampaign_id, segments_dateOne row per campaign per day that had impressions
Advanced report streamsIncremental by dateEntity keys plus segments_dateSame shape and same rules as campaign performance

Column names follow the API’s field paths with dots turned into underscores, so metrics.cost_micros becomes metrics_cost_micros. Google reports money in micros, a millionth of the account currency, and the big integer metrics arrive as strings over REST, so expect to cast and divide. The SQL below does both.

Fact one. A quiet day is a missing row

The Google Ads API returns nothing for a campaign on a day with zero impressions. Not a row of zeros. No row. The connector deliberately lands the data as the API gives it rather than inventing rows, because a fabricated zero is indistinguishable from a real one once it’s in the warehouse.

Daily clicks with missing rows (illustrative)0204060Day 1: 42 clicks1Day 2: 38 clicks2Day 3: 51 clicks3Day 4: 47 clicks4Day 5: 12 clicks5Day 6: no row returned. Zero impressions, so the API omits the day entirely.6Day 7: no row returned. Zero impressions, so the API omits the day entirely.7Day 8: 44 clicks8Day 9: 49 clicks9Day 10: 53 clicks10Day 11: no row returned. Zero impressions, so the API omits the day entirely.11Day 12: 40 clicks12Day 13: 46 clicks13Day 14: 50 clicks14CLICKS BY DAY, ILLUSTRATIVE. DASHED = NO ROW RETURNED
Days 6, 7, and 11 aren’t zeros in the table. They aren’t in the table. An average over the rows you have is an average over eleven days, not fourteen, and a “clicks dropped to zero” monitor never fires because there’s no zero to see.

This breaks three kinds of metric SQL in ways that don’t error.

  • Averages over rows. Average daily clicks over the rows you have is an average over active days only. It runs high, and it runs higher the more days a campaign goes dark.
  • Day-over-day windows. Lag functions and streak logic skip the missing days entirely, so “yesterday” might be four days ago.
  • Zero-spend alerts. A monitor that fires when spend hits zero never fires, because there is no zero. The campaign that got paused by accident on Friday stays invisible.

The fix is a date spine. Build the list of days you care about, cross it with the entity table, and left join the performance rows onto it. Anything that doesn’t match gets a real zero. This is the Postgres version. Snowflake and BigQuery have their own series generators, and the shape is identical.

with days as (
  select d::date as day
  from generate_series(
    current_date - interval '30 days',
    current_date - interval '1 day',
    interval '1 day'
  ) as d
),
perf as (
  select
    campaign_id,
    segments_date::date as day,
    metrics_impressions::bigint as impressions,
    metrics_clicks::bigint as clicks,
    metrics_cost_micros::bigint / 1e6 as spend,
    metrics_conversions as conversions
  from my_google_ads.campaign_performance
)
select
  c.campaign_id,
  c.campaign_name,
  days.day,
  coalesce(p.impressions, 0) as impressions,
  coalesce(p.clicks, 0) as clicks,
  coalesce(p.spend, 0) as spend,
  coalesce(p.conversions, 0) as conversions
from my_google_ads.campaigns c
cross join days
left join perf p
  on p.campaign_id = c.campaign_id
 and p.day = days.day
order by c.campaign_id, days.day;

Note the join direction. Start from the entity table and join performance onto it, never the reverse, because a campaign that hasn’t served in the whole window is still a campaign and belongs on the spine with thirty zeros.

Fact two. Last week’s numbers will change

Conversions attribute after the click, sometimes weeks after. Google keeps restating past days as those conversions land. So a row for August 3 that said four conversions on August 4 might say seven by August 20, and both were correct when read.

This is why the connector re-reads a lookback window behind the cursor on every sync, thirty days by default and up to ninety, and upserts by campaign and date. The restated rows overwrite the old ones in place. You don’t need to do anything for the table to be right.

What you do need to do is stop treating recent conversion numbers as final in anything downstream. Three habits help.

  1. Report cost per conversion on a trailing window that ends a week or two ago, and label the most recent days as provisional.
  2. Don’t snapshot conversion rows into your own history tables. Recompute from the source table, which is the one that gets corrected.
  3. If your conversion window is long, raise the lookback on the connection. A ninety-day lookback costs a few more API operations per sync and nothing else.

Part two. Make it useful

Once the gap-filled model exists, it’s a dataset like any other in the workspace. Define spend, clicks, and cost per conversion as metrics over it, and the rest of the platform picks them up. Monitors watch them overnight, the feed gets a post when something moves more than usual, and the morning brief carries a line if that movement needs a decision. A paused campaign that spent nothing yesterday now produces a real zero, which is the thing a monitor can see.

A good first monitor is simply daily spend per campaign against its own recent history, with the anomaly detection left on default. A good second one is cost per conversion on the trailing fourteen days ending seven days ago, which is far enough back that restatement has mostly settled.

Meta and Local Services, briefly

The Meta Ads and Google Local Services connectors use the same wizard and the same account picker. Two differences are worth knowing.

Meta doesn’t issue refresh tokens. Its long-lived tokens last about sixty days, so Dashfeed re-exchanges them in the background before they expire. If one dies anyway, because someone changed a password or removed the app, the source card switches to “Reconnect required” with a button that reauthorizes in place. Nothing else about the connection changes.

Local Services lands leads and lead conversations rather than campaign performance. Lead status moves over time as leads get charged or credited, and the connector re-reads a thirty-day window for the same reason the Google Ads one does. Conversation text is customer contact data landing in your warehouse, so treat that table the way you’d treat any other table with personal information in it.

Ad spend in the same brief as everything else

Connect Google Ads, Meta, and Local Services in the same afternoon, and let the monitors watch spend overnight.