Google's Ads Transparency Center lets anyone see the ads an advertiser runs across Search, YouTube, Display, Shopping and Maps. It is a great research tool, but it has no alerts and no export, so keeping up with five competitors means clicking through five advertiser pages and trying to remember what you saw last week.
The Ads Transparency actor reads the same public ad library and returns one row per ad, with format, first and last shown dates, landing domain, media URLs and a link to the ad's page. Its monitoring mode remembers every ad it has delivered and only returns new ones on the next run, without charging for the rest.
This guide builds a daily competitor ad monitor: choosing how to identify competitors, setting filters, scheduling, sending new ads somewhere useful and analyzing the history.
Step by step
Decide how to identify each competitor
You have three options.
domains(for example nike.com) returns every ad Google shows for that domain, including ads placed by agencies and resellers.advertiserNames(for example HubSpot) matches the company name as shown in the Transparency Center and keeps all of a brand's legal entities.advertiserIdstakes the AR… ID from an adstransparency.google.com/advertiser/AR… link and is the most precise. Domains are the easiest starting point; switch to IDs if a domain search pulls in advertisers you do not care about.Narrow by market and format
Use
regionswith two-letter country codes to keep only ads shown in your markets, andformats(text, image, video) if you only care about, say, video creatives.dateFromanddateTolimit results to ads shown in a date window, useful for a first backfill. Leave filters empty to see everything.Cap the volume per competitor
maxAdsPerAdvertisercounts ads read newest first for each domain, name or ID, andmaxResultscaps the whole run. For a daily monitor a small per-advertiser cap such as 20 to 50 keeps runs fast and cheap, because new ads appear at the top of the list. Raise it once for an initial backfill if you want the history.Switch on monitoring mode
Set
onlyNewSinceLastRunto true and choose amonitorNameper watch list, for example uk-competitors. Every delivered ad is remembered in the key-value store ads-seen-<monitorName>, and later runs skip it at no charge. Decide whether you needincludeDetails: it adds countries shown, impression ranges and best-effort headline and description text for $0.001 per ad, which is worth it for creative research but not essential for alerts.{ "domains": ["competitor-one.com", "competitor-two.com"], "advertiserNames": ["HubSpot"], "regions": ["GB"], "formats": [], "maxAdsPerAdvertiser": 30, "maxResults": 500, "includeDetails": false, "onlyNewSinceLastRun": true, "monitorName": "uk-competitors" }Schedule it daily
Save the input as an Apify task and attach a schedule with a cron expression like
0 8 * * *. The first run returns the current set of ads up to your caps; from then on, each run returns only ads that appeared since. Add a webhook on run success that calls your own endpoint with the run's dataset ID.Send new ads to Slack
In the webhook handler, fetch the run's dataset and post a short summary to a Slack incoming webhook. Empty runs post nothing, so the channel only lights up when a competitor launches something.
import os import requests # Called from your webhook handler with the finished run's dataset ID def post_new_ads(dataset_id: str, slack_webhook_url: str) -> None: items = requests.get( f"https://api.apify.com/v2/datasets/{dataset_id}/items", params={"clean": "true"}, headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"}, timeout=60, ).json() if not items: return lines = [f"*{len(items)} new competitor ads*"] for ad in items[:20]: lines.append(f"{ad.get('advertiserName')} | {ad.get('format')} | " f"{ad.get('landingDomain')} | {ad.get('previewUrl')}") requests.post(slack_webhook_url, json={"text": "\n".join(lines)}, timeout=30)Analyze format mix and long-running creatives
Once you have a backfill, load it into pandas to see each advertiser's format mix and which creatives have stayed live longest. Long-running ads are usually the ones a competitor keeps paying for, so they are a good place to start when studying their messaging.
import os import pandas as pd from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("actor-forge/ads-transparency").runs().list(desc=True, limit=1, status="SUCCEEDED").items[0] ads = pd.DataFrame(client.dataset(run["defaultDatasetId"]).iterate_items()) ads["firstShown"] = pd.to_datetime(ads["firstShown"]) ads["lastShown"] = pd.to_datetime(ads["lastShown"]) ads["daysLive"] = (ads["lastShown"] - ads["firstShown"]).dt.days # Format mix per advertiser print(ads.pivot_table(index="advertiserName", columns="format", values="previewUrl", aggfunc="count", fill_value=0)) # Long-running creatives are usually the ones that perform print(ads.sort_values("daysLive", ascending=False) [["advertiserName", "format", "daysLive", "landingDomain", "previewUrl"]] .head(20))
Sample input
{
"domains": [
"nike.com"
],
"advertiserNames": [
"HubSpot"
],
"regions": [],
"maxAdsPerAdvertiser": 20,
"maxResults": 200,
"includeDetails": false,
"onlyNewSinceLastRun": true,
"monitorName": "competitors-daily"
}Sample output
Rows look like this, one per ad creative, with a link to the ad's own Transparency Center page:
| advertiserName | format | firstShown | lastShown | regions | landingDomain | previewUrl |
|---|---|---|---|---|---|---|
| Nike, Inc. | text | 2022-11-30 | 2026-10-19 | US | nike.com | https://adstransparency.google.com/advertiser/AR16735076323512287233/creative/CR00144196208338927617 |
| INITIATIVE MEDIA B.V. | video | 2023-10-17 | 2026-10-19 | FR, BE, DE, IT, ES, … | nike.com | https://adstransparency.google.com/advertiser/AR11154049584043393025/creative/CR03397377851928870913 |
| Hubspot, Inc. | text | 2025-11-03 | 2026-10-19 | US, DE, FR, IN, BR, … | hubspot.com | https://adstransparency.google.com/advertiser/AR10072600183532683265/creative/… |
| Argos Limited | text | 2025-… | 2026-10-19 | GB | argos.co.uk | https://adstransparency.google.com/advertiser/AR08556219150266007553/creative/CR07375270183734083585 |
Run it from code
cURL
curl -X POST "https://api.apify.com/v2/acts/actor-forge~ads-transparency/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domains": ["nike.com"], "advertiserNames": ["HubSpot"], "regions": [], "maxAdsPerAdvertiser": 20, "maxResults": 200, "includeDetails": false, "onlyNewSinceLastRun": true, "monitorName": "competitors-daily"}'JavaScript (Node.js)
// Node.js 18+ (built-in fetch). Set APIFY_TOKEN in your environment.
const input = {
"domains": [
"nike.com"
],
"advertiserNames": [
"HubSpot"
],
"regions": [],
"maxAdsPerAdvertiser": 20,
"maxResults": 200,
"includeDetails": false,
"onlyNewSinceLastRun": true,
"monitorName": "competitors-daily"
};
const res = await fetch(
"https://api.apify.com/v2/acts/actor-forge~ads-transparency/run-sync-get-dataset-items",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APIFY_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
}
);
const items = await res.json();
console.log(items.length, items[0]);Python
# pip install apify-client (set APIFY_TOKEN in your environment)
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"domains": [
"nike.com"
],
"advertiserNames": [
"HubSpot"
],
"regions": [],
"maxAdsPerAdvertiser": 20,
"maxResults": 200,
"includeDetails": False,
"onlyNewSinceLastRun": True,
"monitorName": "competitors-daily"
}
run = client.actor("actor-forge/ads-transparency").call(run_input=run_input)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item)The run-sync-get-dataset-items endpoint waits for the run (up to 5 minutes) and returns the dataset rows directly. For longer runs, start the run asynchronously and read the dataset when it finishes, as the Python example does. Replace APIFY_TOKEN with your own token from the Apify console.
What it costs
Ads cost $0.0006 each and details $0.001 each, so a first run of 100 ads is $0.06, or $0.16 with details. After that, a daily monitor that finds around 20 new ads costs about $0.012 a day, roughly $0.36 a month.
| Charged for | When | Price | Per 1,000 | Worked example |
|---|---|---|---|---|
| Ad result | Charged per ad delivered | $0.0006 | $0.60 | 100 ads = $0.06; 1,000 ads = $0.60; a daily monitor returning ~20 new ads = $0.012 a day |
| Ad detail | Charged per ad, only when 'Include ad details' is on (one extra lookup: regions, impressions, ad copy) | $0.001 | $1 | 100 ads with details = $0.06 + $0.10 = $0.16 |
| Actor Start (platform start fee) | Charged when the Actor starts running. Number of events charged depends on Actor memory (one event per GB, minimum one event). | $0.00005 | — | One run at 256 MB = $0.0000125 |
Ready to try it? Google Ads Transparency Scraper — Competitor Ads | $0.60/1K runs on Apify, pay per result.
Run on Apify → Read the full actor pageTips and pitfalls
- There is no keyword search over ad text in the Transparency Center, so you cannot ask for all ads mentioning a phrase. Build your list from competitor domains and advertiser names instead.
- Many Google text ads are published only as a rendered image. Even with details on, expect some text ads to have a preview image and no headline text.
- Advertiser country is filled for advertiser-name searches only; domain and ID searches leave it empty.
- If Google shows a CAPTCHA or unusual-traffic page, the run stops early with the ads already delivered and says so in the status message. Blocked requests are free; just let the next scheduled run continue.
- Use separate
monitorNamevalues for separate watch lists so their seen-ads memories do not overlap.
FAQ
Will I see ads from agencies running campaigns for my competitor?
Yes, if you search by domain. A domain search returns ads from every advertiser that advertises that domain, including agencies and resellers. An advertiser-name or ID search returns only that advertiser's ads.
Can I reset the monitor and start over?
Use a new monitorName. The seen-ads memory is stored per name, so a fresh name starts with an empty history while the old one stays intact.
Can I see how many impressions a competitor's ad got?
With includeDetails on, the actor returns the impression ranges Google publishes per country, along with first and last shown dates per country. These are ranges, not exact numbers.
How far back can I go?
The actor reads ads newest first as the Transparency Center lists them, up to maxAdsPerAdvertiser per search. Use dateFrom and dateTo to focus on a window, and raise the cap for a one-off backfill.
Actors used in this guide
Google Ads Transparency Scraper — Competitor Ads | $0.60/1K
Scrape competitor ads from the Google Ads Transparency Center by domain, advertiser name or ID: format, first/last shown, regions, impressions, ad…
$0.60 per 1,000 · ad resultRelated guides
How to Track Google Trends Trending Searches by Country
Run the ActorForge Google Trends actor with a list of country codes, schedule it daily with only-new mode, and keep a small pandas history to see…
How to Export Shopify Store Product Data to CSV or JSON
Give the ActorForge Shopify Store Scraper a store URL, let it page through the public products.json feed, then download the dataset as CSV or JSON…
How to Audit Every URL in Your Sitemap and Find Orphan Pages
Run the SEO Audit Crawler with useSitemap on and maxPages above your site's size; it audits every page it can reach plus every sitemap URL, and…