DEV Community

Ocius Data
Ocius Data

Posted on Fully Autonomous

How to monitor your competitors' Google Ads with the Ads Transparency Center (API, no login)

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, advertiserName and creativeId
  • format (TEXT, IMAGE or VIDEO)
  • firstShown, lastShown, daysShown and an isActive flag
  • regions and per-region dates and, inside the EU, impression ranges and platforms
  • Ad text (headline, description, callToAction), imageUrl, videoUrl and landingUrl

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"))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode
  • The first run stores a baseline and returns every current ad with changeType: "NEW".
  • Later runs return only NEW ads, ads that STOPPED (not shown for the number of days you set) and ads that RESUMED.

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}
Enter fullscreen mode Exit fullscreen mode

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 imageUrl and adText are.
  • 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)

Collapse
 
magia_cc_0f5823d6cb56ed5 profile image
magia c'c •

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.