Skip to main content
Thirdwatchthirdwatch
Social media

Cross-Channel Ad Intelligence: Google and Meta Ad Libraries

Join Google Ads Transparency Center records with Meta Ad Library data to see a competitor's whole paid footprint, channel mix and launch timing in one table.

Sep 8, 2026 · 6 min read · 1,327 words
See the scraper →

Thirdwatch runs one actor per ad archive: the Google Ads Transparency Scraper for Google Search, Display and YouTube, and the Facebook Ad Library Scraper for Meta. Run both against the same competitor list and you get a channel-mix view no single archive can produce: who is investing where, which channel they launch on first, and whether a creative push is Google-led or social-led.

Why combine Google and Meta ad data

A single-channel view of a competitor is a guess dressed as analysis. If you only watch Meta, a rival who shifts budget into Search looks like a rival who stopped advertising. If you only watch Google, a social-first brand looks like it barely spends. The two archives together cover the overwhelming majority of where a consumer brand's paid money actually goes: Alphabet reported $264.6 billion in Google advertising revenue for 2024 in its annual results, and Meta reported $160.6 billion in advertising revenue for the same year in its own results, which is over $425 billion of ad market between two publicly searchable archives.

The interesting signal is not either channel's total. It is the ratio and the sequence. A brand that launches new creative on Meta two weeks before the equivalent message appears on Google Display is running a social-first testing loop, and you now know where to watch for their next positioning change. A brand whose Google presence is entirely text format while their Meta library is full of video is running Search as a harvesting channel and social as a demand-generation channel, which tells you a great deal about where their growth is actually coming from.

Neither platform hands you spend. Both hand you enough structure to infer strategy, which for competitive planning is usually the more actionable half.

How does this compare to the alternatives?

Cross-channel ad intelligence is the flagship feature of the expensive tools. The archives are free; the work is joining them.

Approach Pricing Channel coverage Setup time Maintenance
Browsing both archives manually Free Both, but no history and no joins Zero 2-3 hours per competitor per cycle
Multi-channel ad intelligence platform Enterprise annual contract Broad, plus estimated spend models 2-6 weeks onboarding Vendor-managed
Two Thirdwatch actors plus a join script Pay per result Google and Meta, at your own cadence About an hour Rerun both on a schedule

Platforms like the enterprise ad-intelligence suites add modelled spend estimates, which is genuinely useful and genuinely a model rather than a measurement. If you need directional spend, buy one. If you need to know what is actually running, when it launched, and how the mix is shifting, the archives are primary sources and the join is fifty lines of code.

How to build a cross-channel view in 4 steps

Step 1: How do I map competitors to both identifiers?

Google is keyed on the destination domain; Meta is keyed on the page or brand name. Build the mapping once.

export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"
COMPETITORS = {
    "Warby Parker": {"domain": "warbyparker.com", "page": "warby parker"},
    "Zenni Optical": {"domain": "zennioptical.com", "page": "zenni optical"},
    "EyeBuyDirect": {"domain": "eyebuydirect.com", "page": "eyebuydirect"},
    "GlassesUSA": {"domain": "glassesusa.com", "page": "glassesusa"},
}

This mapping is the whole join key. There is no shared identifier between the two archives, so the brand-level dictionary is what makes the comparison possible at all.

Step 2: How do I pull the Google side?

Send the domain list to the transparency actor.

import os
import requests

TOKEN = os.environ["APIFY_TOKEN"]
GOOGLE_ACTOR = "thirdwatch~google-ads-transparency-scraper"

google_rows = requests.post(
    f"https://api.apify.com/v2/acts/{GOOGLE_ACTOR}/run-sync-get-dataset-items",
    params={"token": TOKEN},
    json={
        "domainsOrAdvertisers": [c["domain"] for c in COMPETITORS.values()],
        "region": "US",
        "maxResultsPerQuery": 250,
        "proxyConfiguration": {"useApifyProxy": True},
    },
    timeout=900,
).json()

print(f"{len(google_rows)} Google creatives")

Each row carries the query that produced it, so mapping back to the competitor is a dictionary lookup on the domain.

Step 3: How do I pull the Meta side?

Send the page names to the Ad Library actor. It returns one item per brand rather than one per creative.

META_ACTOR = "thirdwatch~fb-ad-library-scraper"

meta_rows = requests.post(
    f"https://api.apify.com/v2/acts/{META_ACTOR}/run-sync-get-dataset-items",
    params={"token": TOKEN},
    json={
        "brands": [c["page"] for c in COMPETITORS.values()],
        "country": "US",
        "adType": "all",
        "maxCreatives": 50,
        "proxyConfiguration": {"useApifyProxy": True},
    },
    timeout=900,
).json()

for item in meta_rows:
    print(f"{item['brand']:20} {item['adCount']} active ads")

Note the shape difference. Google returns a flat list of creatives; Meta returns a brand object with a nested creatives array. The join has to flatten one side.

Step 4: How do I build the channel-mix table?

Aggregate both sides to the brand level and put them next to each other.

import collections

domain_to_brand = {c["domain"]: name for name, c in COMPETITORS.items()}
page_to_brand = {c["page"]: name for name, c in COMPETITORS.items()}

google_mix = collections.defaultdict(collections.Counter)
for row in google_rows:
    brand = domain_to_brand.get(row["query"])
    if brand:
        google_mix[brand][row["format"]] += 1

meta_totals = {}
for item in meta_rows:
    brand = page_to_brand.get(item["brand"])
    if brand:
        meta_totals[brand] = item["adCount"]

header = f"{'Brand':16} {'G:text':7} {'G:image':8} {'G:video':8} {'Meta ads':9}"
print(header)
for brand in COMPETITORS:
    counts = google_mix.get(brand, collections.Counter())
    print(
        f"{brand:16} {counts['text']:<7} {counts['image']:<8} "
        f"{counts['video']:<8} {meta_totals.get(brand, 0):<9}"
    )

Read the table by row, not by column. A brand with a hundred Google text creatives and twelve Meta ads is Search-led. A brand with four Google creatives and two hundred Meta ads is social-led. A brand heavy in both is either large or in an expensive, contested category. Run the same script weekly and the interesting number becomes the change, not the level.

Sample output

The Google side of the join, as returned by the transparency actor:

{
  "query": "shopify.com",
  "region": "US",
  "advertiser_id": "AR01625195283841286145",
  "advertiser_name": "Shopify Inc.",
  "advertiser_domain": "shopify.com",
  "creative_id": "CR00954591614368481281",
  "format": "text",
  "first_shown": "2024-08-05T20:21:48+00:00",
  "last_shown": "2026-09-08T12:22:14+00:00",
  "image_url": "https://tpc.googlesyndication.com/archive/simgad/1939841750827355845",
  "advertiser_url": "https://adstransparency.google.com/advertiser/AR01625195283841286145?region=US",
  "url": "https://adstransparency.google.com/advertiser/AR01625195283841286145/creative/CR00954591614368481281?region=US",
  "source": "Google Ads Transparency Center"
}

And the Meta side, as returned by the Ad Library actor:

{
  "brand": "warby parker",
  "country": "US",
  "adType": "all",
  "active": true,
  "adCount": 156,
  "creativeCount": 50,
  "creatives": [
    {
      "startDate": "May 20, 2026",
      "platforms": ["Facebook", "Instagram"],
      "cta": "Shop Now",
      "textSnippet": "Try 5 frames at home for free."
    }
  ]
}

The asymmetry is the thing to design for. Google gives you a precise ISO timestamp for first_shown and one row per creative, so it supports exact launch-date analysis. Meta gives you a human-readable startDate, an aggregate adCount and a nested sample of creatives with copy and call-to-action text, so it supports messaging analysis. Join on brand, compare each channel against its own history, and never subtract one channel's count from the other's.

Common pitfalls

Treating the two counts as comparable. Meta's adCount is active ads on a page right now. The Google archive includes creatives that stopped serving years ago. One is a live gauge, the other is a history, and a ratio between them is meaningless.

Mapping brands by string similarity. Page names and domains diverge more than you expect, especially for brands with regional pages. Build the mapping by hand once and store it; a fuzzy match will silently attach the wrong competitor's data.

Drifting country filters. Google's region filters where the creative was shown and Meta's country filters the ad's target country. They are near-equivalents, not equivalents, and changing either mid-pipeline breaks comparability with your own history.

Expecting ad copy from the Google side. Google's records carry format and preview, not headline text. If your analysis is about messaging, Meta carries the copy and Google carries the timing.

Thirdwatch keeps both actors on the same output conventions, so the two datasets land in your warehouse with predictable, named fields.

Related use cases

Frequently asked questions

Can I join Google and Meta ad records on a shared key?

Not on a record key. Google keys on advertiser and destination domain, Meta keys on the Facebook page. Join at the brand level instead by mapping each competitor to both a domain and a page name, then compare aggregates rather than individual creatives.

Which channel gives richer creative detail?

Meta gives you ad copy, call-to-action label and platform placement per creative. Google gives you advertiser identity, format and precise first and last shown dates. They are complementary, and neither publishes spend or impressions for commercial ads.

How do I compare volume fairly between the two?

You cannot compare raw counts. Meta reports an active ad count for a page while Google returns creatives from an archive that includes retired ones. Compare within a channel over time, not across channels at a point in time.

Do I need two separate runs?

Yes. They are two actors with different inputs: domains for the Google side, brand or page names for the Meta side. Run both against the same competitor list and join the outputs in your own code.

Which countries can I compare across both?

Both accept country filters, but they are not identical scopes. Google filters on where the creative was shown, and Meta filters on the ad's target country. Keep both fixed across runs so your time series stays internally consistent.

Related

Try it yourself

100 free credits, no credit card.

About 30 real searches. Add the MCP to Claude or Cursor in two minutes.