Disclosure: I build and maintain the Apify Actor used in this tutorial (ocius-data/google-ads-transparency-reliable).
Since 2023, Google publishes every ad shown through Google Ads in the Ads Transparency Center: text, image and video ads, the countries where each one ran, and the first and last date it was shown. It is the most complete free source of competitor ad intelligence available, and it has one problem: there is no official API and no export button. You can browse one advertiser at a time, and that is all.
This tutorial shows how to turn it into a dataset you can query, and how to receive only the ads that started or stopped since the last check.
What you can get
For each ad:
-
advertiserId,advertiserNameandcreativeId -
format(TEXT,IMAGEorVIDEO) -
firstShown,lastShown,daysShownand anisActiveflag -
regionsand per-region dates and, inside the EU, impression ranges and platforms - Ad text (
headline,description,callToAction),imageUrl,videoUrlandlandingUrl
With the optional full-creative mode, every variation of the ad: all headlines, all descriptions, every image and video.
You need Python 3.11 or later, a free Apify account with its API token (Console, Settings, API and Integrations) and the official client: pip install "apify-client>=3" pandas.
Step 1: all the ads pointing to a domain
The most useful query is a domain, not an advertiser name. A domain returns every ad that sends traffic to that website, from all the advertiser accounts behind it, including agencies and resellers.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("ocius-data/google-ads-transparency-reliable").call(run_input={
"searchQueries": ["nike.com"],
"region": "US",
"maxAdsPerAdvertiser": 100,
})
for ad in client.dataset(run.default_dataset_id).iterate_items():
print(ad["format"], ad["daysShown"], ad.get("headline"), ad.get("landingUrl"))
maxAdsPerAdvertiser is enforced in the code, so you never pay for more ads than you asked for. Inputs with no ads, invalid domains and errors are reported in the run summary and are free.
Step 2: answer real questions with the data
Load the results into pandas and a few lines answer the questions a media buyer actually asks:
import pandas as pd
df = pd.DataFrame(client.dataset(run.default_dataset_id).iterate_items())
# Which formats does the competitor invest in?
print(df["format"].value_counts(normalize=True).round(2))
# Long-running ads are usually the winners: the advertiser keeps paying for them.
winners = df[df["isActive"]].sort_values("daysShown", ascending=False)
print(winners[["daysShown", "headline", "landingUrl"]].head(10))
# Which landing pages receive paid traffic?
print(df["landingUrl"].str.split("?").str[0].value_counts().head(10))
The "days shown" ranking is the most valuable view for creative research: an ad that has run for 400 days is one the advertiser keeps paying for, which is the closest public signal to "this ad converts".
Step 3: track changes instead of downloading everything again
Downloading every ad every day is wasteful. Monitoring mode keeps a snapshot per advertiser and returns only what changed:
run = client.actor("ocius-data/google-ads-transparency-reliable").call(run_input={
"searchQueries": ["competitor-a.com", "competitor-b.com", "competitor-c.com"],
"maxAdsPerAdvertiser": 200,
"monitorChanges": True,
"monitorStoreName": "my-competitors",
"stoppedAfterDays": 3,
})
for row in client.dataset(run.default_dataset_id).iterate_items():
print(row["changeType"], row["advertiserName"], row.get("headline"))
- The first run stores a baseline and returns every current ad with
changeType: "NEW". -
Later runs return only
NEWads, ads thatSTOPPED(not shown for the number of days you set) and ads thatRESUMED.
Save this input as a task in Apify Console and schedule it daily. Connect the run to a webhook, Slack, Google Sheets, Zapier, Make or n8n and you have a competitor ad alert system that costs cents per day.
Step 4: full creatives for a swipe file
When you need every variation of each ad (for example, to build a swipe file or to feed creatives to an AI model for analysis), turn on fullCreative:
run_input = {"searchQueries": ["nike.com"], "region": "US",
"maxAdsPerAdvertiser": 50, "fullCreative": True}
Each row then includes allHeadlines, allDescriptions, allImageUrls and allVideoUrls.
Cost
At the time of writing: USD 0.0015 per ad saved (details included), USD 0.003 extra per ad with full creatives, and USD 0.002 per STOPPED or RESUMED row in monitoring mode. A weekly check of five competitors with 50 ads each costs about USD 0.38. Check the Actor page for current pricing.
Limits worth knowing
- Impression ranges and platforms are published by Google only for ads shown in the EU.
- Some text ads exist only as rendered images; their text is not available as structured fields, but
imageUrlandadTextare. - The Transparency Center shows ads, not spend or targeting.
Try it: Google Ads Transparency Center API on Apify. Feature requests are welcome in the Actor's Issues tab.
Runnable versions of every example in this article: apify-data-apis-examples on GitHub.
This article was drafted with AI assistance and reviewed, with every code sample tested against the live API.
Top comments (1)
For monitoring, does maxAdsPerAdvertiser also cap the snapshot used to detect STOPPED ads? A completion flag would matter there: reaching the limit or failing a page should leave missing creatives as unknown, not turn them into stopped campaigns. The NEW/RESUMED distinction is useful only if the comparison knows whether both snapshots were complete. For a BrowserAct Cloud Bot feeding n8n, I'd ask for snapshot completeness with the result too.