Blog · PPC reporting

PPC reporting tool: one live dashboard across Google Ads and Meta

Published July 27, 2026 · 9 min read

Almost every PPC reporting tool is the same three things in a trench coat: a set of platform connectors, a chart builder, and a per-seat invoice. The connectors are the only part that's genuinely hard, the chart builder is the part you'll fight with, and the invoice is the part that decides whether reporting for a fourth client is worth doing at all. This guide takes the pieces apart — what PPC reporting actually has to solve, why cross-platform numbers disagree, and how to assemble reporting that your own AI agent drives from the terminal instead of you driving a UI.

What a PPC reporting tool actually has to solve

Strip away the feature grids and there are only three real jobs:

  1. Get the data out, repeatedly. Not one export — a scheduled pull that survives token expiry, API quotas, and the platform restating yesterday's numbers.
  2. Make the platforms comparable. Google Ads and Meta each have their own idea of a conversion, a click, and a currency-denominated value. Until those are reconciled, "which channel has better CPA" is not a question your data can answer.
  3. Deliver something that stays true. A PDF is wrong the day after you send it. A link that re-queries live data isn't.

Everything else — themes, logo placement, drag-and-drop widgets — is packaging. Most tools spend their roadmap on the packaging because it demos well, and leave you to discover that job #2 was never really done.

Why your PPC numbers never match

If you've ever put Google Ads and Meta side by side in a spreadsheet and had the totals refuse to reconcile, you already know the failure modes. They're worth naming, because a reporting tool that doesn't handle them is just automating a wrong answer faster:

  • Attribution windows differ, and they move. Meta re-attributes conversions over a trailing window of roughly a week. The ROAS you reported on Monday is not the ROAS the platform will show you on Friday for the same days.
  • Conversion definitions differ. A platform-reported "purchase" and your GA4 conversion event are different populations counted by different rules. Neither is lying; they're answering different questions.
  • Derived metrics get re-derived badly. CPA is cost over conversions — but averaging a column of per-campaign CPAs is not the same as dividing total cost by total conversions, and spreadsheets make that mistake silently and constantly.
  • Freshness is uneven. Ad platforms report within hours; Search Console finalizes two to three days late. A "yesterday" column that mixes them is not a comparison.

The rule that saves you: normalize on ingest, not in the report. If spend, clicks, conversions and conversion value are reconciled the moment data lands, then every downstream query — every chart, every client answer — is automatically consistent. If you reconcile at report time, you reconcile forever.

A different shape: the tool your agent drives

TableBI is a data backend built for exactly this, and its interface is a CLI rather than a dashboard editor — because the intended operator is your own Claude Code session, not a human clicking through menus. Three moves:

  1. Pipe. Connect Google Ads and Meta once via OAuth. Data lands in a unified facts table and in lossless per-platform raw tables underneath.
  2. Ask. Read-only SQL, from the terminal, over either altitude — unified when you're comparing channels, raw when you're doing forensics on one.
  3. Pin. Turn any query set into a public read-only URL that re-runs itself as data syncs. That's the client deliverable.

Setup is about five minutes:

terminal
# install the CLI, then teach your agent to drive it
npm i -g @tablebi/cli
tablebi login
tablebi install

# one workspace per client — keeps accounts and reports separate
tablebi connect google_ads -w acme
tablebi connect meta_ads -w acme

# anything without a live connector arrives as CSV, same definitions
tablebi connect csv --file tiktok-export.csv --platform tiktok_ads -w acme

tablebi install plants a skill in ~/.claude/skills, which is the step people skip and then wonder why their agent ignores the tool. A binary on your PATH is not the same as an agent knowing it exists.

One definition of CPC, CPA and ROAS

The unified layer ships with dimensions and metric macros shared across every platform, so the arithmetic is never re-invented per report. Ask the backend what it knows:

terminal
tablebi schema -w acme --json

You get the shared dimensions — date, platform, account, campaign, ad_group, ad, channel, country — the shared measures (impressions, clicks, cost, conversions, conversion_value), and the macros that define every rate metric exactly once:

  • cpc(SUM(cost), SUM(clicks)) — cost / clicks
  • cpm(SUM(cost), SUM(impressions)) — 1000 × cost / impressions
  • cpa(SUM(cost), SUM(conversions)) — cost / conversions
  • ctr(SUM(clicks), SUM(impressions)) — clicks / impressions
  • cvr(SUM(conversions), SUM(clicks)) — conversions / clicks
  • roas(SUM(conversion_value), SUM(cost)) — revenue / cost
  • aov(SUM(conversion_value), SUM(conversions)) — revenue / conversions

Because these are macros over sums rather than averages of pre-computed rates, the "average of averages" bug simply can't happen. A cross-platform PPC comparison becomes one query:

claude code → tablebi
# the whole account, both platforms, last 30 days
tablebi ask "SELECT platform,
             SUM(cost) AS spend,
             cpc(SUM(cost), SUM(clicks))              AS cpc,
             cpa(SUM(cost), SUM(conversions))         AS cpa,
             roas(SUM(conversion_value), SUM(cost))   AS roas
             FROM facts
             WHERE date >= (SELECT MAX(date) FROM facts) - 30
             GROUP BY platform ORDER BY spend DESC" -w acme --json

And the question every PPC report is really trying to answer — where is money leaking? — is a sort, not a slide:

claude code → tablebi
# campaigns burning budget with nothing to show for it
tablebi ask "SELECT platform, campaign,
             SUM(cost) AS spend, SUM(conversions) AS conv,
             cpa(SUM(cost), SUM(conversions)) AS cpa
             FROM facts
             WHERE date >= (SELECT MAX(date) FROM facts) - 14
             GROUP BY platform, campaign
             HAVING SUM(cost) > 0
             ORDER BY conv ASC, spend DESC LIMIT 20" -w acme --json

For a quick read without writing SQL at all, tablebi metrics does the common case directly:

terminal
tablebi metrics --metric roas --group-by channel -w acme --json

The part most reporting tools skip: trust metadata

Every --json response carries a trust block alongside the rows: how fresh each connected source is, the lineage of each metric (the actual formula behind the number), and caveats that keep an agent from drawing a confident wrong conclusion — for instance, that Search Console clicks and GA4 sessions are different populations and shouldn't be expected to match.

This matters more with an AI in the loop than it ever did with a human analyst. A human knows Monday's Meta ROAS is provisional. An agent will happily write "ROAS fell 18% week-over-week" about a week that hasn't finished re-attributing — unless the data tells it not to. Check freshness before you report:

terminal
# connected sources + how far behind each one is
tablebi sources -w acme --json

# full rehydrate: definitions, sources, freshness, existing dashboards
tablebi context -w acme --json

Pinning the report: the deliverable is a URL

Here's where the per-seat economics break in your favour. A pinned dashboard stores the query, not a snapshot — so it re-runs against live data every time someone opens it, and "someone" can be your whole client team without buying anyone a viewer licence.

claude code → tablebi
tablebi pin --title "Acme · PPC performance" \
  --widget "Daily spend (30d)::line=SELECT date, SUM(cost) AS spend FROM facts …" \
  --widget "CPA by platform=SELECT platform, cpa(SUM(cost), SUM(conversions)) AS cpa FROM facts …" \
  --widget "Top campaigns by spend=SELECT campaign, SUM(cost) AS spend FROM facts …" \
  -w acme
✓ published → https://dk.tablebi.com/d/dsh_…  (public, read-only, self-refreshing)

Here's a real pinned dashboard, built exactly this way — it isn't a mockup, and it refreshes as its sources sync:

Dashboards aren't write-once. tablebi dashboard list shows what exists and its publish state, set-spec edits the widget set, and annotate attaches your agent's written analysis to the board — so the "why" lives next to the chart instead of in an email the client lost.

When you need the platform's own fields

Unified definitions are the right altitude for "which channel should get the next thousand dollars." They're the wrong altitude for "why did this one ad group's CPC jump" — that question needs the platform's native columns, at full granularity. Those are kept losslessly in the per-platform raw tables, queried the same way:

claude code → tablebi
# raw altitude — nothing averaged away
tablebi ask "SELECT date, SUM(clicks) AS clicks, SUM(impressions) AS imp
             FROM google_ads_raw
             WHERE date >= (SELECT MAX(date) FROM google_ads_raw) - 30
             GROUP BY date ORDER BY date" -w acme --json

Two altitudes, one backend, and the agent picks per question. That's the workflow difference: you stop choosing between a summary tool and a forensic one.

How this compares to the usual options

  • Native platform reporting (Google Ads UI, Meta Ads Manager) — accurate for one platform, useless across two, and no export you don't perform by hand.
  • Looker Studio — free and flexible, but you own the connector maintenance and the blend logic, which is exactly job #2 above. Blends are where cross-platform reports quietly go wrong.
  • Dedicated PPC reporting platforms — they do solve connectors and templates, and they charge per seat or per client for it. That's fine at ten clients and painful at two.
  • Spreadsheets — infinitely flexible, permanently stale, and the natural habitat of the average-of-averages bug.
  • An agent plus a data backend — connectors and definitions are handled, the analysis runs in your own Claude Code under your own key, and the deliverable is a live link. The reasoning is free because you already pay for the agent.

If your reporting problem is more "many clients, one Monday deadline" than "many platforms," the agency reporting tool guide covers the workspace-per-client pattern in detail. If it's specifically Meta that's eating your afternoons, see Meta ads reporting without the spreadsheet grind. And if you want the report to assemble itself on a schedule, that's automated marketing reports.

FAQ

What should a PPC reporting tool actually do?

Three things: pull each platform on a schedule without manual exports, reconcile them onto one set of definitions so CPA means the same thing everywhere, and hand you a link that stays current. Chart styling is the easy part, and the part most tools over-sell.

Can I report on Google Ads and Meta in one view?

Yes — provided spend, clicks, conversions and conversion value are normalized on ingest. TableBI syncs both into one facts table with shared dimensions and shared macros, so a single GROUP BY platform query is a legitimate comparison rather than a spreadsheet guess.

Is there a free PPC reporting tool?

Both platforms ship free native reporting, and Looker Studio is free to use — the cost just relocates to your time maintaining connectors and reconciling numbers that disagree. Price the labour, not the licence.

Why don't my PPC numbers match between platforms?

Mostly attribution windows and restatement. Meta re-attributes conversions over a trailing window of roughly a week, so recent ROAS is still moving. Sync re-pulls that window automatically, and every query returns a freshness block so your agent knows which days are still provisional.

Can I white-label reports for clients?

Each client gets their own workspace, and each pinned dashboard is a public read-only URL you hand over directly. There are no viewer seats, so sharing with a client's whole team costs nothing extra.

Try it

Connect Google Ads and Meta, and pin your first live PPC dashboard today.

terminal
npm i -g @tablebi/cli && tablebi install