# Competitor Ads Spy - Google Ads Transparency Center, Creatives (`snow_leo_data/google-ads-transparency-scraper`) Actor

19,782 ads for one advertiser in one region in 31.5 seconds, 244 regions, 63 columns where the most used rivals show 13. Google Ads library scraper for competitor ad creatives: format, surface and policy codes translated, removed ads with the policy that removed them.

- **URL**: https://apify.com/snow\_leo\_data/google-ads-transparency-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** SEO tools, Lead generation, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.45 / 1,000 ad creatives

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Ads Transparency Center Scraper - competitor ad library, ad creatives, advertiser accounts

Scrape the Google Ads Transparency Center without a login, an API key, a proxy
or a browser. Point the actor at a brand name, a landing page domain, an
advertiser ID or a link you copied from the report, and get back the ad
creatives Google publishes for that advertiser: text, image and video ads, the
first and last day each one ran, how many days it has been running, the image
URL, the Google surface it was shown on, the countries it reached, the targeting
categories the advertiser used, and the legal entity behind the account.

This is competitor ad intelligence and creative research: see what ads your
competitors run on Google Search, YouTube, Shopping, Maps and Play, which
creatives they keep alive for months, which they killed after a day, and which
markets they push in. It is also an ad library for compliance work: the actor
can return the political advertiser register and the ads Google removed for
policy violations, with the policy that was broken and whether a human or an
automated system made the decision.

#### What you get in one run

- **Ad creatives** - id, advertiser, domain, format, first shown, last shown,
  days shown, image and preview URLs, and a link straight back to the ad in
  Google's own report.
- **Advertiser accounts** - one brand runs many of them. The measured example
  is in this README: the word `nike` resolves to **9** advertiser accounts in
  the report, and the biggest of them holds between **9,000 and 10,000** ads
  while the smallest holds one.
- **Removed ads** - creatives Google took down, with the policy category, the
  enforcement means (automated or human) and the decision type (a notice or
  Google's own investigation).
- **Per-country reach** - impression ranges and first/last served dates per
  country, plus the separate EEA and US breakdowns Google publishes.
- **Monitor mode** - a named memory of everything already delivered, so a daily
  watch pays for the new creatives only.

#### Why this one and not a naive scraper

Everything below was measured, and the commands that measure it ship in
`tools/measure.py`. The numbers live in `tools/measurements.json`.

**The report lies by silence, and the page size is the cheapest trap.** Ask for 100 ads per request and you get **100**.
Ask for 101, 120, 150 or 200 and you get an **empty list** and HTTP 200 - not an
error, not a warning. A scraper that
raises its page size to go faster reads that as "this advertiser has no ads".
This actor requests exactly 100 and pages with the report's own cursor.

**Region is not cosmetic, and it is not a country code.** The report is
region-scoped, and the same domain has a different library in every market. For
`nike.com` the report's own totals are **100,000-200,000** ads worldwide,
**8,000-9,000** in the United States, **10,000-20,000** in France and
**400-500** in Japan. The region has to travel inside the query as a numeric
Google geo criterion id and as a list; the two-letter code is refused and the
region field in the request context is accepted and then ignored. This actor
ships the full table of **244** regions and takes country codes, country names
or ids in `regions`, several at once.

**Pagination goes all the way, but only if you pace it.** A full walk of
`monzo.com` returned **1,398** ads over 14 pages in 2.1 seconds. A full walk of
`nike.com` restricted to France returned **19,782** ads over 198 pages in 31.5
seconds. There is no hidden ceiling at 1,000 or 10,000 rows - the cursor simply
runs out. There is, however, a rate limit: a burst of **200** pages
(**20,000** ads) went through in **33.9** seconds, and at **438** pages the
report answered 429. From a home connection the block held for at least **5424**
seconds - the measurement ran out before the report relented. This actor throttles itself, backs off, keeps every row it already
collected, and says so in the run report instead of dying.

**A brand is not an advertiser.** Buying ads under one brand usually means
several advertiser accounts in different countries, plus resellers and
affiliates pointing at the same domain. Searching by `domains` catches all of
them; searching by `advertiserNames` resolves the brand to its accounts and
takes the biggest ones first, with the ad count of each printed in the log.

**Named values instead of raw numbers.** The report speaks in enum numbers:
format `2`, surface `9`, policy `15`. This actor translates them - `image`,
`youtube`, `gambling_and_games` - using the tables Google ships in the report's
own application, so the rows read like data and not like a protocol dump. The dataset has **63** columns. The three most used actors in this niche expose
**13**, **10** and **8** columns in their dataset views.

#### Inputs

Targets, any mix of them:

- `advertiserNames` - brand names. Each is resolved to advertiser accounts,
  biggest first, up to `maxAdvertisersPerName`.
- `domains` - landing page domains such as `nike.com`. Covers every advertiser
  that sends traffic there.
- `advertiserIds` - exact ids like `AR16735076323512287233`.
- `startUrls` - links copied from adstransparency.google.com. Advertiser,
  creative, domain and region are read out of the link; a link to a single
  creative is fetched directly instead of being searched for.

Scope and filters:

- `regions` - country codes, country names or `anywhere`. Several are allowed;
  each is a separate pass and an ad found in two regions is delivered once with
  both listed.
- `adFormats` - `text`, `image`, `video`.
- `surfaces` - `search`, `youtube`, `shopping`, `maps`, `play`.
- `shownAfter`, `shownBefore` - date bounds, `YYYY-MM-DD`.
- `minDaysShown` - keep long runners only.
- `onlyWithImage` - keep creatives that carry a direct image URL.
- `politicalReport` - ask the political section instead of the general one.

Depth, volume and output:

- `resultType` - `ads`, `advertisers`, `both` or `disapproved`.
- `includeDetails` - one extra request per ad for the full creative card.
- `detailConcurrency` - how many detail requests run at once.
- `maxItems` - a limit on **delivered** rows, not on rows walked.
- `maxAdsPerTarget` - a cap per advertiser, domain and region pass.
- `maxAdvertisersPerName` - accounts taken per brand name.
- `onlyNewAds`, `emitUnchanged`, `monitorStoreName` - monitor mode and its
  memory.
- `includeHtml`, `includeRawData`, `compactOutput` - how heavy each row is.

#### Output columns

Ads: `creative_id`, `advertiser_id`, `advertiser_name`, `advertiser_legal_name`,
`advertiser_disclosed_name`, `advertiser_trademark_name`, `domain`, `format`,
`format_id`, `first_shown`, `last_shown`, `days_shown`, `region`, `regions`,
`ad_url`, `advertiser_url`, `image_url`, `preview_url`, `html`, `variations`,
`variations_count`, `targeting`, `category`, `category_id`, `vertical`,
`countries`, `countries_count`, `served_countries`, `eea`, `us`,
`impressions_low`, `impressions_high`, `spend_low`, `spend_high`,
`contains_synthetic_media`, `google_grants_account`, `served_consecutively`,
`advertiser_unverified`, `reported_by_user`, `political_region`,
`reported_time`, `change_type`, `raw`.

Advertisers: `billing_country`, `disclosure_country`, `creatives_total`,
`creatives_low`, `creatives_high`, `impressions`, `matched_name`, `country`,
`unverified`, `political_report_url`, `political_creatives`, `regulatory_ids`,
`candidate_name`, `candidate_office`, `promoter_name`, `promoter_address`.

Removed ads: `disapproved`, `disapproved_at`, `policies`, `violations`.

`targeting` names the categories the advertiser used and whether each was
included or excluded - demographics, geography, contextual, topic of interest,
advertiser list - plus the age brackets and genders where Google publishes
them. `countries` carries one row per country with the impression range, the
first and last served dates and, when Google splits it, the impressions per
surface: `search_desktop`, `search_mobile`, `youtube`, `gmail`, `news` and
thirteen more names.

#### Honest limits

This section exists because the alternative is a buyer discovering these on
their own run.

- **Date filters are applied by this actor, not by the report.** The query has
  its own `servedDateLowerBound` and `servedDateUpperBound`, and measurement
  says they do nothing: 40 rows unfiltered, 40 rows with a lower bound in 2027,
  40 rows with an upper bound in 2020; a third encoding returns zero rows for
  every input. So `shownAfter` and `shownBefore` are honest filters over
  collected rows - which means a narrow window on a large advertiser still
  costs the walk.
- **You cannot search the ad text.** The report has a field for it and answers
  an empty list for every word tried - `insurance`, `sale`, `vote`. Nobody can
  offer real ad-copy search here; if you see it offered, it is filtering after
  the fact, the same as any date filter in this niche.
- **Impressions and spend are mostly empty for commercial ads.** Google
  publishes those numbers for political advertising and for the EEA
  breakdowns. Regular product ads carry dates, formats and countries, not
  volume.
- **No proxy support.** The actor runs on Apify's own egress, which measured
  200 pages in 34 seconds without a block. A competitor actor in this niche
  does offer a proxy input; if your workload needs tens of thousands of ads per
  hour, that is a real difference and this actor will instead stop politely and
  report partial results.
- **No file import for URLs.** Paste them into `startUrls`.
- **Advertiser-level spend statistics are not offered.** The report's
  statistics endpoint answered empty for every request shape tried, and an
  empty answer is not a feature.

#### How the run reports itself

Every run writes a `REPORT` record next to the dataset: targets walked, region
by region, how many ads each pass saw, how many requests were spent, the
report's own estimate of the library size, how many duplicate rows were dropped
before you were charged, and what every filter removed. When the dataset ends up
empty the report names the reason - `nothing-changed`, `filtered-out`,
`source-returned-nothing`, `source-refused` or `charge-limit-reached` - and the
log says the same thing in words. An empty dataset is normal in monitor mode
and it should never look like a failure.

#### Monitor mode

Turn on `onlyNewAds` and schedule the run. The actor keeps a named key-value
store with a fingerprint of everything it has already delivered; the
fingerprint is built only from fields that arrive in every mode, so switching
`includeDetails` on and off does not make every ad look changed. New and
changed creatives are delivered and charged, unchanged ones are skipped, and
`change_type` says which is which. Give each watch its own `monitorStoreName`
when you follow several competitors.

The memory holds 40,000 keys and drops the oldest beyond that, which is about
3 MB against the platform's 9 MB per-record limit.

#### FAQ

**Is this affiliated with Google?** No. It reads a public transparency report
that Google publishes, the same pages a person sees at
adstransparency.google.com.

**Do I need an API key, a login or a proxy?** No. The actor uses the standard
library only and runs in 1024 MB.

**How many ads can I get?** As many as the report has: the cursor exhausts
naturally. Measured walks: 1,398 ads for a small advertiser, 19,782 for one
market of a big one. Very large libraries - the 100,000-200,000 range - will hit
the report's rate limit before they finish; the actor delivers what it
collected and says why it stopped.

**Why does my advertiser have fewer ads than I expected?** Two usual reasons.
The region narrows the library, and a brand runs several advertiser accounts -
check the log, it prints every account found for a brand name with its ad
count.

**Why is `image_url` empty on some rows?** Text ads and video ads have no
static image. Use `preview_url`, or turn on `includeDetails` to get every
rendering variation.

**Can I get the YouTube video of a video ad?** You get the creative's rendering
variations and the preview URLs Google serves. The underlying video id is not
part of the public payload for every ad.

**What does `contains_synthetic_media` mean?** Google flags creatives that
contain AI-generated content. The flag is carried through as it comes.

**How do I watch a competitor daily without paying twice?** `onlyNewAds: true`,
a schedule, and a `monitorStoreName` per competitor.

**What happens if the report blocks the run?** The actor stops that target,
keeps everything already collected, writes `source-refused` into the report and
exits successfully. Retry later: on Apify's egress the block cleared within a
minute, while a home connection that had hammered the report stayed blocked for
the full 5424 seconds of the measurement.

# Actor input Schema

## `advertiserNames` (type: `array`):

Brand names to look up in the report, for example "Nike" or "Booking.com". One brand usually runs several advertiser accounts (Nike has nine in the report, from 9,000 ads down to one), so the actor takes the biggest ones - see maxAdvertisersPerName - instead of guessing.

## `domains` (type: `array`):

Landing page domains, for example nike.com. A domain covers every advertiser account that sends traffic there, including resellers and affiliates, so it returns far more than a single advertiser id.

## `advertiserIds` (type: `array`):

Exact advertiser ids from the report, for example AR16735076323512287233.

## `startUrls` (type: `array`):

Paste links from adstransparency.google.com. Advertiser, creative, domain and region are read out of the link.

## `resultType` (type: `string`):

ads - ad creatives; advertisers - advertiser accounts behind the brand names; both - advertisers first, then their ads; disapproved - ads Google removed for a policy violation, with the policy, the enforcement means and the decision type. Note on `disapproved`: that report is Google's own global takedown feed, so its rows carry the creative, the policy and the dates, but NO advertiser id or name - they cannot be tied back to one competitor.

## `regions` (type: `array`):

Country codes or names, or "anywhere" for the worldwide view. The report is region scoped: nike.com shows 100,000-200,000 ads worldwide but 8,000-9,000 in the US and 400-500 in Japan. Several regions in one run are allowed - each is a separate pass and the same ad found in two of them is delivered once, with both regions listed.

## `adFormats` (type: `array`):

text, image or video. Leave empty for all three.

## `surfaces` (type: `array`):

Where the ad was shown: search, youtube, shopping, maps, play. Leave empty for all of them.

## `shownAfter` (type: `string`):

YYYY-MM-DD. Keeps ads whose last recorded impression is on or after this date. Applied by the actor after collecting, because the report accepts its own date bounds and then ignores them - measured, see the README.

## `shownBefore` (type: `string`):

YYYY-MM-DD. Keeps ads that started running on or before this date.

## `minDaysShown` (type: `integer`):

Keep only ads that ran at least this many days. A long runner is a tested creative; a one-day ad is usually a test.

## `onlyWithImage` (type: `boolean`):

Keep only creatives that carry a direct image URL.

## `includeDetails` (type: `boolean`):

One extra request per ad. Adds every rendering variation, the targeting categories used, age and gender conditions, the product category, per country impression ranges with first and last served dates, the EEA and US breakdowns, the AI-generated content flag and the legal entity name. On by default: without it a row carries 12 fields instead of 22, and the extra columns are the reason to use this Actor. Measured: no extra time on a first run.

## `detailConcurrency` (type: `integer`):

How many detail requests run at once. Eight is measured as safe; sixteen starts getting rate limited.

## `politicalReport` (type: `boolean`):

Ask the political section of the report instead of the general one. Advertiser rows then carry the regulator id (FEC and the EU equivalents), the declared candidate or ballot measure and the promoter.

## `onlyNewAds` (type: `boolean`):

Remembers what earlier runs delivered in a named key-value store and writes only what is new or changed. A daily watch on a competitor then pays for the few new creatives instead of the whole library.

## `emitUnchanged` (type: `boolean`):

In monitor mode, deliver unchanged ads too, tagged with change\_type. Off by default because unchanged rows cost money and say nothing.

## `maxItems` (type: `integer`):

Hard limit on DELIVERED rows, not on rows walked: with filters on, the actor keeps walking until it has this many rows that actually passed. Zero means no limit.

## `maxAdsPerTarget` (type: `integer`):

Cap per advertiser, domain or region pass, so one huge advertiser cannot eat the whole run. Zero means no cap.

## `maxAdvertisersPerName` (type: `integer`):

How many advertiser accounts to take for each brand name, biggest first.

## `includeHtml` (type: `boolean`):

Keep the raw HTML snippet of the creative. Off by default: it is the heaviest field and most buyers only need the image URL.

## `includeRawData` (type: `boolean`):

Attach the untouched response object to every row.

## `compactOutput` (type: `boolean`):

Drop empty fields and the heavy HTML and variation lists. Useful when the rows go into an AI agent with a token budget.

## `monitorStoreName` (type: `string`):

Name of the named key-value store that remembers what monitor mode already delivered. Give each watch its own name when you track several competitors, otherwise they share one memory.

## Actor input object example

```json
{
  "advertiserNames": [],
  "domains": [
    "nike.com"
  ],
  "advertiserIds": [],
  "startUrls": [],
  "resultType": "ads",
  "regions": [
    "anywhere"
  ],
  "adFormats": [],
  "surfaces": [],
  "onlyWithImage": false,
  "includeDetails": true,
  "detailConcurrency": 8,
  "politicalReport": false,
  "onlyNewAds": false,
  "emitUnchanged": false,
  "maxItems": 50,
  "maxAdsPerTarget": 0,
  "maxAdvertisersPerName": 3,
  "includeHtml": false,
  "includeRawData": false,
  "compactOutput": false
}
```

# Actor output Schema

## `results` (type: `string`):

All collected rows

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "advertiserNames": [],
    "domains": [
        "nike.com"
    ],
    "advertiserIds": [],
    "startUrls": [],
    "regions": [
        "anywhere"
    ],
    "adFormats": [],
    "surfaces": [],
    "includeDetails": true,
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/google-ads-transparency-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "advertiserNames": [],
    "domains": ["nike.com"],
    "advertiserIds": [],
    "startUrls": [],
    "regions": ["anywhere"],
    "adFormats": [],
    "surfaces": [],
    "includeDetails": True,
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/google-ads-transparency-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "advertiserNames": [],
  "domains": [
    "nike.com"
  ],
  "advertiserIds": [],
  "startUrls": [],
  "regions": [
    "anywhere"
  ],
  "adFormats": [],
  "surfaces": [],
  "includeDetails": true,
  "maxItems": 50
}' |
apify call snow_leo_data/google-ads-transparency-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/google-ads-transparency-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/GxuuTJLuZQguC0leJ/builds/Nm58ksee5rvMpokg8/openapi.json
