Google publishes every ad it serves in a public registry, the Transparency Center, and offers no way to download it. This repository is a compact, tested Python client for the Google Ads Transparency Center Scraper on Apify, which does the downloading for you. Hand it a competitor's domain such as nike.com, or a Google advertiser ID, and it returns a table of that advertiser's ads: which creative, in which format, where the click lands, when Google first and last showed it, and how many impressions it reports. The collection happens on Apify's servers, so your script only starts a run and reads the answer. Output lands in results.json and results.csv, ready for pandas, a spreadsheet or a warehouse.
Actor: Google Ads Transparency Center Scraper & API · Publisher: Devil Scrapes · Also in JavaScript: google-ads-transparency-scraper-javascript
- One row per ad creative, naming the advertiser, the creative ID, a link back to the ad, the domain it sends people to, and a format code for text, image or video.
- Lifetime of every ad: Unix timestamps for the first and the last impression Google logged, plus the impression count Google reports where it publishes one.
- Domains or advertiser IDs: look up a brand by the site its ads point to, or by the exact advertiser ID when you already have it, and mix both in one call.
- A shared ad is saved once: when two of your targets surface the same creative, the dataset holds one copy and the bill counts one.
- Screening switch: flip one boolean to get a single summary row per domain (any ads, how many, since when) for checking a long prospect list cheaply.
- Limits you control: a date window, a cap on total rows and a cap on result pages keep a test run small and a large pull predictable.
An ad that keeps running for months has usually earned its budget, and one that vanishes after a week usually has not. Load results.json into a DataFrame, subtract the first date from the last and sort. The top of the list is a reading list of what your competitor trusts.
Before a quarterly meeting, pull the three biggest rivals in one run and save a CSV per brand. The creative links open the original ads in Google's own registry, so every slide can point at a source rather than a screenshot.
Run your own domain on a schedule and compare advertiser names and landing domains with the list you expect. An advertiser you do not recognise sending traffic to a domain that is not yours is worth a closer look.
Turn on screeningMode, paste the domains you plan to contact and keep the ones with has_ads_detected set to true. Companies already paying for Google ads are a sharper target for an agency pitch than a cold list.
You need an Apify account and an API token from Console → Settings → Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.
git clone https://github.com/devilscrapes/google-ads-transparency-scraper-python.git
cd google-ads-transparency-scraper-python
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export APIFY_TOKEN=your_token_here
python main.py # uses input.json
python main.py my-input.json --out data --max-charge 2The script starts the Google Ads Transparency Center Scraper & API, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.
This is all the integration needs. The official apify-client package starts the Actor, waits for the run to end and then reads the rows from the dataset the run produced.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("DevilScrapes/google-ads-transparency").call(run_input={'searchDomains': ['nike.com'], 'region': 'anywhere', 'maxResults': 50, 'maxPages': 2, 'proxyConfiguration': {'useApifyProxy': True}})
for item in client.dataset(run.default_dataset_id).iterate_items():
print(item)main.py reads input.json and forwards it unchanged. The bundled file looks up one brand and stops at 50 ads over at most 2 result pages, so a first run costs about $0.35 (the $0.20 run fee plus 50 ads at $0.003). Add domains or advertiserIds, or set dateFrom and dateTo, when you want a wider pull.
{
"searchDomains": [
"nike.com"
],
"region": "anywhere",
"maxResults": 50,
"maxPages": 2,
"proxyConfiguration": {
"useApifyProxy": true
}
}| Field | Type | Required | Default | Allowed values |
|---|---|---|---|---|
searchDomains |
array | no | ["nike.com"] |
— |
advertiserIds |
array | no | [] |
— |
region |
string | no | "anywhere" |
anywhere, AE, AR, AT, AU, BE, BG, BR, CA, CH, CL, CO, CZ, DE, DK, EE, EG, ES, FI, FR, GB, GR, HU, ID, IE, IL, IN, IT, JP, KR, LT, LV, MX, MY, NL, NO, NZ, PH, PL, PT, RO, SA, SE, SG, SK, TH, TR, UA, US, VN, ZA |
dateFrom |
string | no | — | — |
dateTo |
string | no | — | — |
maxResults |
integer | no | 1000 |
— |
maxPages |
integer | no | 25 |
— |
proxyConfiguration |
object | no | {"useApifyProxy": true} |
— |
screeningMode |
boolean | no | false |
— |
Each row is one ad creative. Dates arrive as Unix seconds exactly as Google reports them, and region simply echoes the label you sent: Google returns the same ads whatever region you pick. With screeningMode on you receive one summary row per domain instead.
| Field | Type | Always present |
|---|---|---|
advertiser_id |
string | no |
advertiser_name |
string | no |
creative_id |
string | no |
creative_url |
string | no |
landing_domain |
string | no |
format_type |
integer or null | no |
first_shown_ts |
integer or null | no |
last_shown_ts |
integer or null | no |
impressions |
integer or null | no |
preview_image_url |
string or null | no |
preview_content_js_url |
string or null | no |
region |
string | no |
scraped_at |
string | no |
domain |
string or null | no |
has_ads_detected |
boolean or null | no |
ad_count |
integer or null | no |
first_seen_ts |
integer or null | no |
last_seen_ts |
integer or null | no |
sample_creative_url |
string or null | no |
| Event | Price (USD) |
|---|---|
| Actor start | $0.2 |
| Ad creative scraped | $0.003 |
Prices as configured on 2026-10-16. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.
Here is a short pandas analysis (pip install pandas). It turns the timestamps into dates, computes how many days each ad was live, labels the format and writes the 25 longest-running ads to a CSV for review.
import json
import pandas as pd
ads = pd.DataFrame(json.load(open("results.json", encoding="utf-8")))
for column in ("first_shown_ts", "last_shown_ts"):
ads[column] = pd.to_datetime(ads[column], unit="s", utc=True)
ads["days_live"] = (ads["last_shown_ts"] - ads["first_shown_ts"]).dt.days
ads["format"] = ads["format_type"].map({1: "text", 2: "image", 3: "video"})
longest = ads.sort_values("days_live", ascending=False).head(25)
columns = ["advertiser_name", "format", "days_live", "impressions", "landing_domain", "creative_url"]
longest[columns].to_csv("longest_running.csv", index=False)
print(longest[columns[:4]].to_string(index=False))Google does not publish one for the Transparency Center. The Actor fills that gap: you call it with an Apify token and receive structured rows, with no Google account, developer key or approval step.
Google stops answering a single query after about 1,000 ads. To reach further, split the pull by date window with dateFrom and dateTo, then merge the files on creative_id.
Not today. The region input is stored on each row as a label so you can organise exports by market, but Google returns the same set of ads for every value. Treat it as bookkeeping, not as a filter.
Video and animated ads have no still image. They carry preview_content_js_url, a link to the script bundle Google uses to draw the ad, while static image ads carry preview_image_url. Showing the video frame itself needs a browser on your side.
Set APIFY_TOKEN first: export the token in the same shell, or put it in a local.envfile your shell loads. Never commit it.- Run finished with
FAILEDorABORTED: open the printed Console link; the run log says why. Input validation errors name the field. - Fewer rows than expected: a
--max-chargecap also limits how many results can be charged. Raise the cap or lower the limits in your input. - Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.
- Google Ads Transparency Center Scraper & API on Apify Store
- JavaScript version of this example
- All Devil Scrapes Actors
- Apify web scraping cookbook
Related Actors:
- Meta Ad Library Scraper — Ad Search & Export — Facebook and Instagram ads, found by advertiser or keyword.
- TikTok Ad Library Scraper — commercial content from TikTok's ad library.
- Pinterest Ad Library Scraper — Ad Search & Export — Pinterest ads grouped by advertiser.
- Bing Ad Library Scraper — Ad Search & Export — the Microsoft Advertising ad library.
- Apple Ad Repository Scraper — App Store Search Ads.
MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Google or the Google Ads Transparency Center.