# Google Ads Transparency Scraper & Change Monitor (`broken-not-dead/google-ads-transparency-scraper-change-monitor`) Actor

Extract every live Google ad creative for any advertiser: image, video and text, with real asset URLs and first/last-seen dates. See what a competitor launched and stopped since your last run. Domain search catches every ad account, incl. agencies. A zero-ad run is never silent: the log says why.

- **URL**: https://apify.com/broken-not-dead/google-ads-transparency-scraper-change-monitor.md
- **Developed by:** [Bujar K](https://apify.com/broken-not-dead) (community)
- **Categories:** Lead generation, Social media, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 returned ads

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### What does Google Ads Transparency Scraper & Change Monitor do?

**Google Ads Transparency Scraper** extracts **every ad creative an advertiser is currently running on Google** — image, video and text — straight from the [Google Ads Transparency Centre](https://adstransparency.google.com). You get the real creative asset URLs, the advertiser account behind each ad, first-seen and last-seen dates, and the country each ad runs in. Search by **domain** (`nike.com`) or by brand name, pick a country, and run it. Because it runs on the Apify platform you also get a REST API, scheduling, webhooks, monitoring, and integrations with Make, Zapier, n8n and Google Sheets.

Try it with the default input — it returns about 55 ads from two advertisers in
about five seconds. The pair is deliberate: nike.com is enormous, so you see how
the Actor behaves when an inventory is bigger than your limit, and brewdog.com is
small enough to scan completely — **run it a second time** and you will see the
change monitor label real launches and pauses.

### Why use Google Ads Transparency Scraper?

Google publishes this data because the EU Digital Services Act requires it. That makes it one of the few genuinely public windows into what your competitors are spending money on right now.

- **See competitors' live creative.** Which headlines, images and videos are running today, not last quarter.
- **Build a creative swipe file.** Every asset URL, ready to download.
- **Track launches and seasonality.** First-seen and last-seen dates show exactly when a campaign started and whether it is still live.
- **Monitor brand and trademark abuse.** Find resellers and affiliates bidding on your brand.
- **Feed an ad-intelligence product.** Stable JSON, incremental runs, and an API — designed to be called on a schedule, not clicked once.

### Ad monitoring: launches **and** pauses, or an honest "not sure"

Most scrapers hand you a snapshot and leave you to diff two multi-thousand-row
files yourself. Some can now tell you what launched.

This one also tells you **what stopped** — and, crucially, refuses to guess when
it cannot be certain.

Turn on **Track changes** (on by default), schedule it daily, and every ad comes
back labelled:

| `change_status` | meaning |
| --- | --- |
| `baseline` | starting point — the first run, or any run that could not scan the full inventory (see below) |
| `unchanged` | still running |
| `new` | launched since your last run — **confirmed** |
| `paused` | was running, now gone — **confirmed** |

Set **Only output what changed** and a scheduled run returns just the launches
and pauses — a clean feed for Slack, a webhook, or a dashboard. Runs with
nothing to report (the first run, a quiet day, or a partial scan) return no
rows and charge only the per-advertiser scan fee.

#### Change labels only appear when they are true

An ad missing from today's results is only genuinely paused **if we looked at
the whole inventory**. If the run stopped at your result limit, a missing ad
might simply be past the limit — and an ad that "appeared" might simply have
been past the limit last time. Google does not even return a stable slice: two
capped scans of the same big advertiser can overlap on a handful of ads.

Most tools cannot tell those apart, so they report false launches and false
pauses. This Actor tracks whether each run reached the end of the advertiser's
inventory. When both the previous and the current run did, you get `new`,
`paused` and `unchanged`, confirmed. When either scan was partial, it does not
guess: every row is labelled `baseline`, nothing is reported as new or paused,
and one line in the run log tells you the advertiser's approximate inventory
and exactly what to set **Max ads per query** to so labels turn on.

A monitor that invents competitor campaign changes is worse than no monitor.

#### Five things this scraper does that most do not

**1. It does not silently lose your video and text ads.** In the Transparency Centre's API, image creatives store their content in one field and video and text creatives store it in a completely different one. A scraper that reads only the image field returns HTTP 200, logs success, and quietly drops every video and text ad — roughly 11% of a typical advertiser's inventory. This Actor reads both paths and reports an **asset-URL fill rate** on every run so you can see it is complete.

**2. Domain search finds every ad account, not just one.** Brands advertise through several Google Ads accounts — regional entities, resellers, and their media agency. Searching `adidas.com` returns creatives from *adidas International B.V.*, *Adidas America Inc*, **and** their agency *MediaCom L.L.C*, in one dataset. Resolving a brand to a single advertiser ID structurally cannot see the rest.

**3. The output is safe to open.** Ad creatives are written by whoever buys the ad, and this Actor's output lands in your spreadsheet, your dashboard and your database. An advertiser that names itself `=cmd|'/c calc'!A1` would execute on your machine the moment you open the CSV in Excel. Advertiser names and domains are defused before they reach you, `javascript:` and `data:` URLs are never emitted as asset links, and newlines and control characters that corrupt CSV rows are stripped. Legitimate names such as `L'Oréal` and `adidas International B.V.` come through untouched.

**4. Filters mean what they say.** Ask for 20 video ads and you get 20 video ads, even if the Actor has to page through 400 ads to find them — not 2 because it stopped counting early.

**5. A quiet run is never a silent failure.** The most common complaint filed against scrapers in this niche is some variant of *"successful run, zero results"* — a green tick, an empty dataset, and no way to tell whether the advertiser has no ads or the scraper broke. This Actor closes that gap: every run logs per-query counts (scanned, pushed, pages), an asset-URL fill rate, and — when a query returns little or nothing — one plain-language line saying **why** (wrong country is the usual culprit) and what to change. The same numbers land in a **RUN\_REPORT** you can check from the API. You are never left guessing whether zero means zero.

### How to use Google Ads Transparency Scraper

1. Click **Try for free**.
2. In **Advertiser domains or names**, enter one or more domains — `nike.com`, `booking.com`. Domains work best.
3. Choose a **Country**. Ads are reported per country; an advertiser may run thousands of ads in one and none in another.
4. Set **Max ads per query**.
5. Click **Start** and download the results as JSON, CSV, Excel or HTML.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `queries` | array | `["nike.com", "brewdog.com"]` | Domains or brand names. Domains are preferred. |
| `region` | string | `GB` | Country code — 110 supported. |
| `maxResultsPerQuery` | integer | `50` | Max ads **returned** per query. |
| `formats` | array | all | `image`, `video`, `text`. |
| `activeWithinDays` | integer | — | Only ads shown in the last N days. Applied on Google's side — far faster and cheaper. Use `7` for a daily monitor. |
| `trackChanges` | boolean | `true` | Label each ad new / unchanged / paused vs your last run. |
| `onlyChanges` | boolean | `false` | Return only launches and pauses. Runs that just record or refresh a baseline (the first run, or any partial scan) return no rows and charge only the scan fee. |
| `onlyAdsShownSince` | string | — | Absolute-date version of `activeWithinDays`. |
| `maxRunMinutes` | integer | `60` | Wall-clock budget for the whole run. Advertisers there was no time to scan are skipped and **not charged**. A full-depth scan takes roughly 90 s per advertiser, so raise this for long lists. |
| `stateStoreName` | string | `ads-transparency-state` | Key-value store holding the change-tracking baselines. Use one store per client to keep tracking histories separate. |
| `includeCreativeHtml` | boolean | `false` | Include the raw HTML fragment for image ads. |
| `minFillRate` | number | `0.5` | Warn if extraction quality drops below this. |

```json
{
    "queries": ["nike.com", "adidas.com"],
    "region": "US",
    "maxResultsPerQuery": 200,
    "formats": ["image", "video", "text"]
}
```

### Output

Each ad creative is one dataset item. You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

```json
{
    "advertiser_id": "AR18378488041124659201",
    "advertiser_name": "Nike Retail BV",
    "creative_id": "CR07657183496217034753",
    "format": "image",
    "first_shown_iso": "2026-07-08T14:18:12+00:00",
    "last_shown_iso": "2026-08-04T18:30:29+00:00",
    "region": "GB",
    "region_count": 28,
    "asset_urls": ["https://displayads-formats.googleusercontent.com/ads/preview/content.js?..."],
    "preview_url": "https://adstransparency.google.com/advertiser/AR18378488041124659201/creative/CR07657183496217034753"
}
```

Every run also writes a **RUN\_REPORT** record to the key-value store with per-query statistics: how many ads were scanned, which advertiser accounts matched, the format breakdown, and the extraction fill rate.

#### Data fields

| Field | Description |
| --- | --- |
| `advertiser_id` | Google advertiser account ID (`AR…`) |
| `advertiser_name` | Advertiser account name |
| `creative_id` | Unique creative ID (`CR…`) — stable, use it to deduplicate |
| `format` | `image`, `video` or `text` |
| `asset_urls` | Direct URLs to the creative assets |
| `render_url` | Google's renderable preview URL (video and text ads) |
| `creative_html` | Raw, UNSANITISED HTML fragment for image ads (optional, off by default — treat as untrusted and escape it before rendering) |
| `first_shown` / `first_shown_iso` | When the ad was first observed |
| `last_shown` / `last_shown_iso` | When the ad was last observed |
| `region` | Country the results were requested for |
| `region_count` | Number of countries this creative runs in |
| `preview_url` | Link to the ad in Google's own Transparency Centre |
| `destination_domain` | The domain the ad points at |
| `change_status` | `new`, `unchanged`, `paused` or `baseline` |
| `query` | The input query that produced this row |

### How much does it cost to scrape Google Ads Transparency Centre?

This Actor talks directly to the Transparency Centre's JSON API. It uses **no headless browser**, which is what makes it cheap and fast — the default run finishes in about five seconds and 40 ads arrive per request.

Pricing is **$0.02 per advertiser scanned + $2.00 per 1,000 ads returned**, so cost scales with what you actually ask for:

| What you run | Cost |
| --- | --- |
| Default run — 2 advertisers, ~55 ads | **$0.15** |
| 1 advertiser, 200 ads | **$0.42** |
| 1 advertiser, 1,000 ads | **$2.02** |
| Daily monitor — 1 competitor, 6 changes | **$0.03** |
| Daily monitor — 10 competitors, ~5 changes each | **$0.30** |

That last row is the point. With **Only output what changed**, a scheduled monitor reads a competitor's inventory and bills you only for the ads that actually changed — runs that merely record or refresh a baseline return no rows and charge only the scan fee. Watching ten competitors every day costs about **$9 a month**.

Apify's free tier is enough to evaluate the Actor properly. There is **no
rental, no subscription and no monthly fee** — you pay per result, and a query
that finds nothing costs the $0.02 scan fee and nothing more.

### Tips and advanced options

- **Use domains, not brand names.** `nike.com` beats `Nike` — it catches every ad account promoting that domain, including agencies.
- **For a daily monitor, use `activeWithinDays` (try 7) — not `onlyAdsShownSince`.** `activeWithinDays` keeps a stable window, so change tracking compares like with like across runs. A fixed calendar date changes the window every day, which starts a fresh baseline each time: you would be billed for every ad, every day, and never see a change reported. Use `onlyAdsShownSince` for one-off historical pulls, not for scheduled monitoring.
- **Deduplicate on `creative_id`.** It is stable across runs.
- **Check `region`.** Most "no results" reports are an advertiser with no ads in the chosen country. Try `US` — it is usually the largest inventory.
- **Watch the fill rate.** The run log prints an asset-URL fill rate. It should be 100%. A sudden drop is an early warning that Google changed its response format.
- **Use a date window instead of pulling everything.** Setting **Only ads active in the last N days** asks Google to filter before sending, which is much faster and cheaper. For nike.com the full history is ~10,000 ads while the last 7 days is ~1,000. For monitoring it is also more accurate, because the full history includes ads that stopped running long ago.
- **Most runs need no proxy. Very large ones may.** Google applies two separate defences: a client-fingerprint check, which this Actor handles for free, and a request-volume limit per IP address. Ordinary runs never come close. Pulling an advertiser's entire 10,000-ad history in one go can trip it — measured, 263 requests in about 70 seconds from a single IP triggers a challenge. If you see a bot-challenge error, either narrow the date window, lower **Max ads per query**, or enable the proxy option. The Actor paces itself automatically on large scans and stops cleanly with the ads it already collected rather than failing outright.

### FAQ, disclaimers and support

**Is scraping the Ads Transparency Centre legal?** The Centre is a public, no-login database that Google is legally required to publish under the EU Digital Services Act. This Actor collects advertising data — no logins and no paywalled content. Note that advertiser identity fields can name a natural person where the advertiser is a sole trader, so treat the output accordingly under GDPR. Ad creatives themselves remain the copyright of the advertisers who made them. You are responsible for how you use the data, including compliance with Google's Terms of Service, copyright, and applicable law in your jurisdiction.

**My run succeeded but returned zero ads — is it broken?** Almost certainly not, and unlike most tools this one tells you why in the run log. The usual cause is the country: ads are reported per country, and an advertiser can run thousands of ads in the US and none in the country you picked. Try `US`, or the advertiser's home market. Two other causes: unverified advertisers may not appear at all, and a brand name matches less than a domain — search `nike.com`, not `Nike`. A zero-ad query costs you $0.02 (the advertiser scan) and nothing per ad.

**Why do I get fewer ads than the Transparency Centre shows?** Google reports an approximate total per country and caps how deep the result set can be paged. Use `maxResultsPerQuery` to control depth, and narrow by country.

**Why is an advertiser missing?** Some advertisers are not verified, and unverified advertisers may not appear. Try the domain rather than the brand name.

**Does it get political ad spend data?** Not yet. Political ads carry extra disclosures (spend and impression ranges) and are on the roadmap.

**Something is broken.** Open an issue on the **Issues** tab — bugs get fixed. If you need a custom field, a different country set, or a tailored integration, that can be arranged too.

# Actor input Schema

## `queries` (type: `array`):

One per line. A domain (nike.com) is strongly preferred — it returns ads from EVERY advertiser account promoting that domain, including media agencies and resellers. A brand name (Nike) is matched to the highest-volume advertiser accounts instead. The default pair is a demo: nike.com is huge (you will see the honest partial-scan warning), brewdog.com is small enough to scan fully — run it twice and the second run labels real launches and pauses.

## `region` (type: `string`):

Ads are reported per country. An advertiser can run thousands of ads in one country and none in another.

## `maxResultsPerQuery` (type: `integer`):

Upper bound on creatives returned for each query. Large advertisers can have hundreds of thousands. Set it above the advertiser's total inventory to enable confirmed change labels (new / paused); a capped scan cannot tell a paused ad from one beyond the limit, so it stays quiet.

## `maxRunMinutes` (type: `integer`):

Hard wall-clock budget for the whole run. Large scans are deliberately paced to avoid Google's per-IP request limit, which makes them slow — roughly 90 seconds per advertiser at full depth. Without this cap, 200 advertisers would run for about five hours. When the budget is reached the run stops cleanly, keeps everything collected so far, and does NOT charge for advertisers it never scanned.

## `formats` (type: `array`):

Which creative formats to keep. All three are extracted correctly; video and text ads store their content differently from images, and are commonly dropped by other scrapers.

## `trackChanges` (type: `boolean`):

Remember each run and label every ad as new, unchanged or paused. The first run records a baseline; every run after that tells you what your competitor launched or killed. Labels are only applied when the run scanned the advertiser's full inventory — on a partial scan every row says 'baseline' and the log tells you what to raise maxResultsPerQuery to, instead of guessing at changes.

## `onlyChanges` (type: `boolean`):

Return only new and paused ads. Runs with nothing to report — the first run, a quiet day, or any partial scan (see trackChanges) — return no rows and charge only the scan fee. The baseline is still updated in full. Ideal for a daily scheduled run feeding Slack, a webhook or a dashboard.

## `stateStoreName` (type: `string`):

Named key-value store holding the baselines. Change it to keep separate tracking histories (for example one per client).

## `activeWithinDays` (type: `integer`):

Ask Google for ads shown in the last N days instead of an advertiser's entire history. This is applied on Google's side, so it is dramatically faster and cheaper: for nike.com the full history is ~10,000 ads while the last day is ~900. It is also more accurate for monitoring, because the full history includes ads that stopped running long ago. Recommended: 7 for a daily monitor. Leave empty for all-time.

## `onlyAdsShownSince` (type: `string`):

Absolute-date version of the option above: return only ads shown on or after this date. Also applied on Google's side. Ignored if 'Only ads active in the last N days' is set. Note that Google evaluates the date in Pacific time, so the boundary is accurate to within a day.

## `includeCreativeHtml` (type: `boolean`):

Adds the raw HTML fragment for image ads. Off by default to keep the dataset small; asset URLs are always included.

## `minFillRate` (type: `number`):

Warn if fewer than this fraction of creatives have an asset URL. A sudden drop means Google changed its response shape and extraction is silently degrading. Lower it only if you know why.

## `proxyConfiguration` (type: `object`):

Optional. Google throttles by request volume per IP address, not by fingerprint — ordinary runs never come close to the limit, so most runs need no proxy. Without one, each query is capped at about 4,800 ads to protect the IP. Enable RESIDENTIAL proxies for very large scans or if a run reports a bot challenge.

## Actor input object example

```json
{
  "queries": [
    "nike.com",
    "brewdog.com"
  ],
  "region": "GB",
  "maxResultsPerQuery": 50,
  "maxRunMinutes": 60,
  "formats": [
    "image",
    "video",
    "text"
  ],
  "trackChanges": true,
  "onlyChanges": false,
  "stateStoreName": "ads-transparency-state",
  "activeWithinDays": 7,
  "includeCreativeHtml": false,
  "minFillRate": 0.5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `creatives` (type: `string`):

No description

## `runReport` (type: `string`):

No description

# 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 = {
    "queries": [
        "nike.com",
        "brewdog.com"
    ],
    "maxResultsPerQuery": 50,
    "maxRunMinutes": 60,
    "formats": [
        "image",
        "video",
        "text"
    ],
    "activeWithinDays": 7
};

// Run the Actor and wait for it to finish
const run = await client.actor("broken-not-dead/google-ads-transparency-scraper-change-monitor").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 = {
    "queries": [
        "nike.com",
        "brewdog.com",
    ],
    "maxResultsPerQuery": 50,
    "maxRunMinutes": 60,
    "formats": [
        "image",
        "video",
        "text",
    ],
    "activeWithinDays": 7,
}

# Run the Actor and wait for it to finish
run = client.actor("broken-not-dead/google-ads-transparency-scraper-change-monitor").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 '{
  "queries": [
    "nike.com",
    "brewdog.com"
  ],
  "maxResultsPerQuery": 50,
  "maxRunMinutes": 60,
  "formats": [
    "image",
    "video",
    "text"
  ],
  "activeWithinDays": 7
}' |
apify call broken-not-dead/google-ads-transparency-scraper-change-monitor --silent --output-dataset

```

## MCP server setup

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

```

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/Lb7w4Q4XpgoqANcpz/builds/1ESpX3KceXdGoDeKd/openapi.json
