Scrape Google Ads Transparency Center for Competitor Ads
Pull every creative a competitor runs on Google Search, Display and YouTube from the Ads Transparency Center as structured JSON, filtered by country and format.

Thirdwatch's Google Ads Transparency Scraper turns Google's public ad archive into structured JSON. Pass competitor domains or advertiser names, pick a country, and get one row per creative with advertiser identity, creative ID, format, first and last shown dates, an image preview where Google exposes one, and links back to the public transparency pages. Built for growth and competitive intelligence teams who need Google-side ad data, not just Meta.
Why scrape Google Ads Transparency Center for competitor research
Most competitive ad research stops at Meta because the Ad Library is easy to browse. That leaves the larger half of the market unobserved. Alphabet reported $264.6 billion in Google advertising revenue for 2024 across Search, YouTube and Network in its annual results, which is more money than any single Meta surface carries. If you only monitor social, you are watching the smaller channel.
The Ads Transparency Center closed that gap. Google publishes a searchable archive of ads served by verified advertisers, addressable by advertiser or by the destination domain the ad points at. The problem is the interface. It is built for one-off lookups: you type a brand, scroll a grid of creative thumbnails, click each one, and read dates off a modal. There is no export, no diff, no way to hold fifty competitors in view at once, and no way to feed the result into a dashboard.
Scraping converts that browsing exercise into a dataset. Once every creative is a row with an advertiser ID, a creative ID and two timestamps, you can count creatives per competitor, rank them by how long they have been running, group them by format, and re-run the whole thing next week to see what changed. That is competitive intelligence. Clicking through thumbnails is not.
How does this compare to the alternatives?
There are three realistic ways to get Google ad creative data at scale, and they differ mostly in who absorbs the maintenance burden when Google changes the Center.
| Approach | Pricing | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| DIY Python scraper | Free plus your engineering time | Breaks whenever Google reshapes the archive | 2-5 days of reverse engineering | Yours, permanently |
| Generic scraping API | Subscription, often per-request | Returns raw HTML you still have to parse | 1-2 days of parser work | Parser is still yours |
| Thirdwatch Google Ads Transparency Scraper | Pay per result | Structured records, schema kept current | Under 10 minutes | Thirdwatch tracks Google's changes |
A DIY build is genuinely achievable for one afternoon and then genuinely annoying forever. The archive is an internal surface with no published contract, so the shape of a response is not something Google owes you stability on. A generic scraping API solves the fetching problem but leaves you owning the parsing problem, which is the part that actually breaks. The actor gives you named fields that stay named.
How to scrape Google Ads Transparency Center in 5 steps
Step 1: How do I get an Apify API token?
Create a free account at apify.com, open Settings, then Integrations, and copy the API token.
export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"No credit card is needed to start, and the token is the only credential the actor requires. You never authenticate against Google.
Step 2: How do I pull every creative for one competitor?
Pass the competitor's destination domain in domainsOrAdvertisers, set a region, and cap the pull with maxResultsPerQuery.
import os
import requests
ACTOR = "thirdwatch~google-ads-transparency-scraper"
TOKEN = os.environ["APIFY_TOKEN"]
resp = requests.post(
f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
params={"token": TOKEN},
json={
"domainsOrAdvertisers": ["nike.com"],
"region": "US",
"maxResultsPerQuery": 200,
"proxyConfiguration": {"useApifyProxy": True},
},
timeout=900,
)
creatives = resp.json()
print(f"{len(creatives)} creatives")
print(creatives[0]["advertiser_name"], creatives[0]["format"])Domains are the sharper input. nike.com matches the verified destination domain on the ad, so you get creatives that actually send traffic to Nike, regardless of which legal entity bought the placement.
Step 3: How do I compare several competitors in one run?
Put the whole competitive set in the array. Each record carries the query that produced it, so the rows stay separable after the run.
import collections
competitor_set = [
"nike.com",
"adidas.com",
"newbalance.com",
"underarmour.com",
]
resp = requests.post(
f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
params={"token": TOKEN},
json={
"domainsOrAdvertisers": competitor_set,
"region": "US",
"maxResultsPerQuery": 150,
"proxyConfiguration": {"useApifyProxy": True},
},
timeout=900,
)
by_brand = collections.Counter(row["query"] for row in resp.json())
for brand, count in by_brand.most_common():
print(f"{brand:20} {count} creatives")Creative volume per brand is a crude but useful proxy for how aggressively each competitor is testing. A brand returning 150 rows against a cap of 150 has more inventory than the cap allows; a brand returning 22 has a genuinely thin Google presence.
Step 4: How do I see the creative format mix?
The format field is one of text, image or video, which maps cleanly to where the ad ran.
import collections
rows = resp.json()
mix = collections.defaultdict(collections.Counter)
for row in rows:
mix[row["query"]][row["format"]] += 1
for brand, counts in mix.items():
total = sum(counts.values())
parts = ", ".join(
f"{fmt} {100 * n // total}%" for fmt, n in counts.most_common()
)
print(f"{brand:20} {parts}")A video-heavy mix means YouTube investment. An image-heavy mix means Display and remarketing. A text-heavy mix means the competitor's Google budget is concentrated in Search, which is usually the highest-intent and most expensive part of their spend.
Step 5: How do I schedule this as a recurring pull?
Register a schedule against the actor so the dataset refreshes without anyone remembering to press run.
curl -X POST "https://api.apify.com/v2/schedules?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "google-ads-competitor-pull-weekly",
"cronExpression": "0 6 * * 1",
"timezone": "America/New_York",
"isEnabled": true,
"actions": [{
"type": "RUN_ACTOR",
"actorId": "thirdwatch~google-ads-transparency-scraper",
"runInput": {
"domainsOrAdvertisers": ["nike.com", "adidas.com", "newbalance.com"],
"region": "US",
"maxResultsPerQuery": 150,
"proxyConfiguration": {"useApifyProxy": true}
}
}]
}'The Apify schedules API accepts standard cron. Weekly is the right default for creative monitoring; see the flight-date guide for when daily is worth it.
Sample output
Three real records from a nike.com run against the US region:
[
{
"query": "nike.com",
"region": "US",
"advertiser_id": "AR18378488041124659201",
"advertiser_name": "Nike Retail BV",
"advertiser_domain": "nike.com",
"creative_id": "CR00559723144491827201",
"format": "text",
"first_shown": "2025-10-22T17:45:31+00:00",
"last_shown": "2026-09-08T12:18:59+00:00",
"image_url": "https://tpc.googlesyndication.com/archive/simgad/3694801686415580819",
"preview_html": "<img src=\"https://tpc.googlesyndication.com/archive/simgad/3694801686415580819\" height=\"154\" width=\"348\">",
"advertiser_url": "https://adstransparency.google.com/advertiser/AR18378488041124659201?region=US",
"url": "https://adstransparency.google.com/advertiser/AR18378488041124659201/creative/CR00559723144491827201?region=US",
"source": "Google Ads Transparency Center"
},
{
"query": "nike.com",
"region": "US",
"advertiser_id": "AR18378488041124659201",
"advertiser_name": "Nike Retail BV",
"advertiser_domain": "nike.com",
"creative_id": "CR05831480404351123457",
"format": "image",
"first_shown": "2021-10-25T07:00:00+00:00",
"last_shown": "2026-09-08T12:18:00+00:00",
"image_url": null,
"preview_html": null,
"advertiser_url": "https://adstransparency.google.com/advertiser/AR18378488041124659201?region=US",
"url": "https://adstransparency.google.com/advertiser/AR18378488041124659201/creative/CR05831480404351123457?region=US",
"source": "Google Ads Transparency Center"
},
{
"query": "nike.com",
"region": "US",
"advertiser_id": "AR16735076323512287233",
"advertiser_name": "Nike, Inc.",
"advertiser_domain": "nike.com",
"creative_id": "CR10898768143992225793",
"format": "image",
"first_shown": "2026-05-05T16:29:59+00:00",
"last_shown": "2026-09-08T12:14:30+00:00",
"image_url": null,
"preview_html": null,
"advertiser_url": "https://adstransparency.google.com/advertiser/AR16735076323512287233?region=US",
"url": "https://adstransparency.google.com/advertiser/AR16735076323512287233/creative/CR10898768143992225793?region=US",
"source": "Google Ads Transparency Center"
}
]Three things to read here. advertiser_id differs between the first two records and the third, so one domain is served by more than one advertiser entity. first_shown on the second record is 2021, which means that creative has been in rotation for years. And image_url is null on two of the three, because Google does not return an inline preview asset for every creative; the url field still opens the creative on Google's own page.
Common pitfalls
Assuming one domain means one advertiser. It does not. A nike.com pull returns Nike Retail BV, Nike, Inc., a Singapore branch entity and media agencies, all pointing at the same domain. Group by advertiser_id, not by brand name, before you count anything.
Expecting a preview image on every row. Google returns an inline preview for some creatives and a rendered-only preview for others, so image_url and preview_html are null on a meaningful share of records. Filter on image_url is not None before building anything visual, and use url as the universal fallback.
Treating the pull as a census. maxResultsPerQuery caps at 500 and large advertisers exceed that. A run is a sample of the archive, ordered by Google, not the complete set.
Using a region code the actor does not map. Region accepts two-letter ISO codes; an unrecognised code falls back to searching all regions, which silently widens your result set. Lock the code for the life of a pipeline.
Thirdwatch's actor handles the fetching, pagination and field normalisation so your pipeline receives the same named fields on every run.
Related use cases
- Google Ads Transparency Scraper
- Track competitor Google ad launches and flight dates
- Verify which advertisers run Google Ads on a domain
- Build a Google display ad creative swipe file
- Cross-channel ad intelligence with Google and Meta
- Monitor competitor Facebook ad campaigns
- The complete guide to scraping business data
- All Thirdwatch use-case guides
Frequently asked questions
Do I need a Google Ads account to use the Ads Transparency Center?
No. The Ads Transparency Center is a public archive Google publishes for anyone. The actor reads the same public records you would see in a browser, so no Google Ads login, API access or advertiser relationship is required.
Does the Ads Transparency Center show ad spend or impressions?
No. Google does not publish spend, impressions or click data for commercial ads in the Center. You get advertiser identity, creative identity, format and the first and last dates the creative was seen serving.
Can I search by brand name instead of domain?
Yes. The domainsOrAdvertisers input accepts either. Domains are more precise because they match the verified destination domain, while names match the advertiser's registered legal entity and can return unrelated matches.
How many creatives can I pull per advertiser?
Up to 500 per query via maxResultsPerQuery. Large advertisers have far more creatives than that in the archive, so treat a single run as a ranked sample rather than a complete census of everything they have ever run.
Does the region input change which fields I get back?
No. Region filters which creatives are returned based on where they were shown. Every record carries the same fields regardless of country, though preview assets are missing more often for some formats than others.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.