Want to know what your competitors say in their Google Ads? Google will show you. Since 2023 the Google Ads Transparency Center lists every ad a verified advertiser runs on Search, YouTube, Display, Shopping and Maps, with the dates it was shown and the countries it ran in.
The catch: it's built for looking up one advertiser at a time in a browser. There's no export and no API. And for most text ads, Google doesn't store the ad as text at all. It stores a screenshot of the rendered ad, so even if you scrape the page, you get a PNG instead of a headline.
This tutorial shows how to get competitors' ads as clean JSON or CSV, with the headline, description, display URL and sitelinks as real text. There are three examples: a CSV of competitors' ads, a check of which companies in a list run Google Ads at all, and a Slack alert for new ads. It uses the Google Ads Transparency Scraper on Apify, which I built for exactly this. All code is in the examples repo.
What you get per ad
A real record from a run on 2026-10-02:
{
"advertiserName": "Hubspot, Inc.",
"domain": "hubspot.com",
"format": "TEXT",
"firstShown": "2025-10-15",
"lastShown": "2026-10-02",
"daysShown": 322,
"headline": "SEO Keyword Research Tools - Track Your SEO Progress",
"description": "HubSpot's SEO tools help you research keywords, track rankings & optimize content. Get actionable SEO...",
"displayUrl": "www.hubspot.com/",
"sitelinks": ["Free Website Builder", "Sign Up Free", "HubSpot SEO Tools", "Improve Your Site Rank", "Free Content Tools"],
"adCopySource": "ocr",
"imageUrl": "https://tpc.googlesyndication.com/archive/simgad/10713166194209069207",
"adUrl": "https://adstransparency.google.com/advertiser/AR10072600183532683265/creative/CR03473167961525583873?region=US"
}
Video ads come with the YouTube link (videoUrl) and a thumbnail. Image ads come with the image URL.
How the ad copy is read
The Transparency Center's web app gets its data from a JSON endpoint, so listing an advertiser's ads with dates and formats is the easy part. The text is harder. For 60–99% of text ads (depending on the advertiser), the only thing Google returns is a PNG of the ad as it appeared on the search page.
The layout of that image is always the same: advertiser name and display URL at the top, then the headline in Google's blue, then the description in grey, then optional sitelinks ending in >. So the scraper runs OCR (Tesseract) on the image and uses each line's text colour to tell the headline from the description. On clean renders like these, OCR is very accurate for English and the other Latin-script languages. adCopySource tells you whether the copy came from OCR or from Google's own preview data (used for video, shopping and local ads).
Option 1: no code
- Open the Google Ads Transparency Scraper and click Try for free. A free Apify account is enough.
- Enter one or more advertisers: a domain (
hubspot.com), a brand name (HubSpot) or a Transparency Center link. - Optionally pick a country, an ad format, and Shown in the last N days.
- Click Start, then download the results as CSV, Excel or JSON.
Option 2: Python, competitors' ads as CSV
Install the Apify client and set your API token (Console → Settings → Integrations):
pip install apify-client
export APIFY_TOKEN=...
This script collects the text ads three CRM companies ran in the US in the last 30 days, sorted by how long each ad has been running. An ad that has run for months or years is one that works, which makes it the most useful thing to learn from:
import csv
import os
from decimal import Decimal
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("plain-signal/google-ads-transparency-scraper").call(
run_input={
"advertisers": ["hubspot.com", "pipedrive.com", "salesforce.com"],
"country": "US",
"adFormat": "TEXT",
"shownInLastDays": 30,
"maxAdsPerAdvertiser": 50,
},
max_total_charge_usd=Decimal("0.30"), # 150 ads × $0.002
)
ads = list(client.dataset(run.default_dataset_id).iterate_items())
ads.sort(key=lambda a: a["daysShown"] or 0, reverse=True) # ads that run for months are the ones that work
with open("competitor_ads.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["advertiser", "headline", "description", "display_url", "first_shown", "last_shown", "days_shown", "ad_url"])
for a in ads:
w.writerow([a["advertiserName"], a["headline"], a["description"], a["displayUrl"], a["firstShown"],
a["lastShown"], a["daysShown"], a["adUrl"]])
print(f"{len(ads)} ads saved to competitor_ads.csv")
for a in ads[:5]:
print(f"{a['daysShown']:>4} days {a['advertiserName']}: {a['headline']}")
Output from a run on 2026-10-02:
150 ads saved to competitor_ads.csv
1556 days Hubspot, Inc.: HubSpot CRM Platform - Grow Better - Schedule a Demo Today
1556 days Hubspot, Inc.: HubSpot CRM Platform - Simplify Your Processes - Grow Better
1556 days Hubspot, Inc.: HubSpot CRM Platform
1552 days Hubspot, Inc.: HubSpot Sales Hub - Powerful Free Sales Tools
1540 days Hubspot, Inc.: Hubspot Service Hub - Get Started Today
HubSpot's "Grow Better - Schedule a Demo Today" headline has been running since June 2022. The run took under three minutes, most of it OCR.
Two things worth knowing:
-
max_total_charge_usdis a hard cap on what the run can cost. The scraper stops cleanly when it's reached. -
A domain search also finds other advertisers. Searching
hubspot.comreturned 42 ads from HubSpot itself plus a few from small advertisers whose ads point to HubSpot-hosted pages. Filter onadvertiserName, or pass the advertiser ID (fromadUrl) instead of the domain to get one account only.
Option 3: which companies run Google Ads?
For lead generation (agencies, ad-tech and martech sales), the question is often just "does this company spend money on Google Ads?". Advertiser check mode returns one row per input, with Google's estimate of how many ads they have:
import os
from decimal import Decimal
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
domains = ["hubspot.com", "pipedrive.com", "close.com", "attio.com", "folk.app", "copper.com", "nutshell.com",
"capsulecrm.com", "streak.com", "lessannoyingcrm.com"]
run = client.actor("plain-signal/google-ads-transparency-scraper").call(
run_input={"advertisers": domains, "country": "US", "mode": "advertisers"},
max_total_charge_usd=Decimal("0.05"), # 10 checks × $0.002
)
rows = list(client.dataset(run.default_dataset_id).iterate_items())
for r in sorted(rows, key=lambda r: r["adCountMax"] or 0, reverse=True):
if r["isAdvertising"]:
ads = f"{r['adCountMin']:,}–{r['adCountMax']:,} ads" if r["adCountMin"] != r["adCountMax"] else f"{r['adCountMax']} ads"
print(f"{r['input']:<22} yes {ads:<16} last shown {r['lastShown']} ({', '.join(r['formats'])})")
else:
print(f"{r['input']:<22} no")
Output (it took about a second):
hubspot.com yes 1,000–2,000 ads last shown 2026-10-02 (IMAGE, TEXT, VIDEO)
pipedrive.com yes 1,000–2,000 ads last shown 2026-10-02 (TEXT)
attio.com yes 200–300 ads last shown 2026-10-02 (IMAGE, TEXT, VIDEO)
nutshell.com yes 100–200 ads last shown 2026-10-02 (IMAGE, TEXT, VIDEO)
streak.com yes 100–200 ads last shown 2026-10-02 (IMAGE, TEXT, VIDEO)
close.com yes 93 ads last shown 2026-10-02 (IMAGE, TEXT, VIDEO)
copper.com yes 90 ads last shown 2026-10-02 (IMAGE, TEXT, VIDEO)
folk.app yes 62 ads last shown 2026-10-02 (TEXT)
capsulecrm.com yes 21 ads last shown 2026-10-02 (IMAGE, TEXT)
lessannoyingcrm.com yes 16 ads last shown 2026-10-02 (TEXT)
Google gives large counts as ranges (1,000–2,000), and exact numbers below 100. Feed in a few hundred domains from your CRM and you have a list of companies that are actively buying ads, and roughly how much.
Option 4: a Slack alert for competitors' new ads
The scraper has a monitor mode: give it a monitorName, and each run checks each competitor's newest ads and returns only the ones earlier runs with that name haven't returned. Schedule it daily or weekly:
import json
import os
import urllib.request
from decimal import Decimal
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("plain-signal/google-ads-transparency-scraper").call(
run_input={
"advertisers": ["monday.com", "asana.com", "clickup.com"],
"country": "US",
"maxAdsPerAdvertiser": 30, # look at each competitor's 30 newest ads
"monitorName": "pm-tools", # remembers what earlier runs returned
},
max_total_charge_usd=Decimal("0.20"),
)
ads = list(client.dataset(run.default_dataset_id).iterate_items())
lines = []
for a in ads:
what = a["headline"] or (f"video: {a['videoUrl']}" if a["videoUrl"] else a["format"].lower() + " ad")
lines.append(f"*{a['advertiserName']}*: {what}\n{a['adUrl']}")
text = f"{len(ads)} new competitor ads\n\n" + "\n\n".join(lines) if ads else "No new competitor ads."
webhook = os.environ.get("SLACK_WEBHOOK_URL")
if webhook:
req = urllib.request.Request(webhook, data=json.dumps({"text": text}).encode(),
headers={"Content-Type": "application/json"})
urllib.request.urlopen(req)
else:
print(text)
The first run returns everything in the window (90 ads in my test). Later runs return only what's new. One a few minutes later found a single new ad:
1 new competitor ads
*Mango Technologies, Inc.*: video: https://www.youtube.com/watch?v=Ac_HTGC2Lyg
Mango Technologies is ClickUp's legal name, which is a nice side effect: the Transparency Center shows the verified company behind each brand. Schedule the script with cron or GitHub Actions. If you'd rather not run code at all, create a schedule in the Apify Console and connect the Slack, Google Sheets or email integration to the actor.
What does it cost?
The scraper charges $0.002 per ad ($2 per 1,000), with the ad copy included, and $0.002 per advertiser in advertiser-check mode. There's no monthly fee. The 150-ad CSV above costs $0.30, the 10-domain check $0.02, and a weekly competitor monitor usually comes to cents a month. Apify's free plan includes monthly platform credit, which is enough to try all of the above.
Good to know
-
Languages: OCR works well for Latin-script languages (English, German, French, Spanish, Italian, Portuguese, Dutch, Polish). For Japanese, Chinese or Arabic ads, the text fields may be empty or garbled;
imageUrlstill has the ad. -
Dynamic keyword insertion: some ads use placeholders like
{KeyWord:Nike Dunk}. You get the default text (Nike Dunk). -
Brand names: a name like
Salesforceis matched to advertisers with exactly that name (ignoring "Inc.", "Ltd" and the like). If there's no exact match, the scraper uses the matching domain and says so in the log. For full control, use a domain or an advertiser ID. - Coverage: only verified advertisers are listed, and political ads are in a separate Google report that isn't included.
-
Dates:
firstShownandlastShowncome from Google, anddaysShownis Google's count of days the ad was shown.
Wrapping up
All three examples are in the plain-signal/examples repo, along with examples for the Google Jobs Scraper and the Funding Rounds Tracker. Ads, hiring and funding together make a strong lead signal: a company that just raised, is hiring marketers and has started running Google Ads is spending to grow. Questions and feature requests are welcome in the comments or on the actor's Issues tab.
This article was written with AI assistance. All code was run against the live scraper before publishing, and the outputs shown are real.
Top comments (0)