# Google Ads Transparency Scraper & Ad Monitor (`singletonops/google-ads-transparency-scraper`) Actor

Monitor competitor Google ads: export any advertiser's Search, YouTube, Shopping, Maps & Play ads from the Ads Transparency Center with headlines, OCR text for image ads, CTAs, regions and first/last shown dates. Monitor mode outputs only new or changed ads. No login.

- **URL**: https://apify.com/singletonops/google-ads-transparency-scraper.md
- **Developed by:** [SingletonOps](https://apify.com/singletonops) (community)
- **Categories:** Marketing, SEO tools, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 Scraper & Ad Monitor

**Monitor competitor Google ads** from the public Google Ads Transparency Center: every Search, YouTube, Shopping, Maps and Play ad an advertiser runs, with headlines, OCR text for image ads, CTAs, regions and first/last shown dates. Monitor mode outputs only new or changed ads. No login. Built by SingletonOps (a brand of Vibex LLC).

### Quickstart: your first run in 2 minutes

1. Click **Try for free** (free Apify account, no card needed).
2. The form is already filled in: `nike.com`, region **US**, ads shown in the **last 30 days**, **20 ads**, creative details and OCR on. Keep it, or replace `nike.com` with your competitor's domain.
3. Click **Start**. Our test of this input (without the date filter) returned 20 ads in 11 seconds.
4. Open the **Output** tab: headline, description, CTA, format, platforms and first/last shown dates per ad. Download as CSV, Excel or JSON.
5. To keep watching: **Save as task**, turn on **Only new or changed ads**, and add a weekly **schedule**. Later runs output only `new`, `changed` and `stopped` ads.

```json
{
  "advertisers": ["nike.com"],
  "region": "US",
  "dateFrom": "30 days",
  "maxAds": 20
}
```

A 20-ad first run costs about $0.03 at the proposed price (see [Pricing](#pricing); the Pricing tab on the Store page is authoritative).

### Sample output (real run)

Real rows from a small test run on 2026-10-12 (input `{"advertisers": ["nike.com"], "region": "US", "maxAds": 20}`, 20 ads in 11 s). Text is shortened here; all 20 full records are in [`samples/first-run-nike-us-20-ads_2026-10-12.json`](samples/first-run-nike-us-20-ads_2026-10-12.json), unedited except that the newer `firstSeenDate`, `lastSeenDate` and `daysRunning` fields were added offline from each row's own `firstShown`/`lastShown`. 19 of the 20 ads have been running for 180+ days, so `longRunningOnly: true` would keep 19.

| format | headline | description | CTA | textSource | firstShown | servedDays | daysRunning | platforms |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| TEXT | Nike® Official Site | Nike® offers ultimate selection of gear, accessori… | website | native | 2023-11-16 | 870 | 1062 | MAPS, SEARCH, SHOPPING, YOUTUBE |
| TEXT | Shop The Newest Nike® Styles | Nike® Kevin Durant basketball shoes for every athl… | website | native | 2023-11-17 | 901 | 1061 | MAPS, SEARCH, SHOPPING, YOUTUBE |
| TEXT | Nike® Golf Pants & More | Hit the links with the latest Nike® golf gear to c… | website | native | 2023-11-16 | 900 | 1062 | MAPS, SEARCH, SHOPPING |
| IMAGE | LeBron James Los Angeles Lakers City Edition … | — | — | native | 2026-06-03 | 91 | 132 | SEARCH, SHOPPING, YOUTUBE |
| TEXT | Kids Crew Socks. Nike.com | Shop The Official Nike Store For The Latest Nike S… | — | ocr | 2022-11-30 | 1362 | 1413 | MAPS, SEARCH, SHOPPING, YOUTUBE |
| TEXT | Shop Nike® Socks - Shop Nike® Online - Get Ac… | Comfortable, breathable, and performance focused. … | — | ocr | 2023-11-16 | 899 | 1062 | MAPS, SEARCH, SHOPPING, YOUTUBE |
| TEXT | — | Nike Clothing Is Designed For Performance And Tota… | website | native | 2022-11-30 | 1359 | 1413 | MAPS, SEARCH, SHOPPING, YOUTUBE |

The last row is a Maps/local ad with a dynamic headline: Google stores no headline text for it, so `headline` is `null` rather than guessed.

A domain returns **every advertiser account** whose ads point to it: this run found 3 (Nike, Inc. with 18 ads, Nike Retail BV and Nike Japan with 1 each). Group or filter by `advertiserName`, or use an `AR…` ID for one account. On your own domain, that's how you spot resellers and affiliates.

Monitor-mode rows look the same plus `changeType` and `changedFields`. Real example: [`samples/monitor-rerun-zalando-de-first3_2026-10-09.json`](samples/monitor-rerun-zalando-de-first3_2026-10-09.json): the first 3 rows of a real second monitor-mode run on Zalando SE in Germany, with that run's `OUTPUT` counts. Unchanged ads are checked but not saved, so they cost nothing.

Data comes from the public [Google Ads Transparency Center](https://adstransparency.google.com/). Search by **advertiser name, website domain or advertiser ID** and get clean, structured ad data: headlines, descriptions, images, YouTube videos, first and last seen dates, regions, platforms and impression ranges. Turn on **monitor mode** to get only the ads that are **new or changed** since the last run.

Use it as a **Google ad library / ad spy tool** for PPC competitor research: see the exact ad copy, creatives, CTAs and run dates of any competitor, export them to CSV/Excel/JSON, and get alerted when they launch new ads.

No browser, no login, no Google account. The actor calls the same lightweight JSON endpoints the Transparency Center website uses, so runs are fast and cheap.

***

### What does Google Ads Transparency Center Scraper do?

1. Resolves each input to Google advertiser IDs: names (fuzzy-matched, with the strongest advertiser picked), domains (every advertiser that runs ads for that site), IDs (`AR…`) or full Transparency Center URLs.
2. Lists that advertiser's ads with your filters: **region** (any of 250 countries), **date range**, **format** (text, image, video) and **platform** (Search, YouTube, Shopping, Maps, Play).
3. For each ad (optional, on by default) it fetches the creative details and the ad preview, and pulls out the headline, description, call to action, display URL, landing domain, image URLs and YouTube video URLs, plus per-region statistics (first and last shown, impression ranges, platforms). Text ads that Google archived only as a screenshot are read with **OCR** (on by default, see below).
4. In **monitor mode**, it compares each ad with a snapshot from the previous run and outputs only `new` and `changed` ads (plus `stopped` ones, see the FAQ), along with the exact fields that changed.
5. Writes a run summary (`OUTPUT`) and a data-quality report (`QUALITY_REPORT`) to the key-value store.

### Use cases for PPC teams

- **Monday new-ad check without the clicking.** Schedule a weekly run in monitor mode (`onlyNewOrChanged`) for each competitor domain and read only the `new`, `changed` and `stopped` rows, with `changedFields` showing what moved.
- **Find the ads worth copying.** Turn on `longRunningOnly` (or set `minDaysRunning`, e.g. 90) to keep only ads a competitor has kept running for 6+ months, the ones their tests keep picking. Every row has `firstSeenDate`, `lastSeenDate` and `daysRunning`; sort by `daysRunning` or `numServedDays`.
- **See when a promotion really ended.** `lastSeenDate` is the last day Google showed the ad, so a sale ad whose `lastSeenDate` stopped moving has stopped running, whatever the landing page still says.
- **Catch offer and landing-page changes.** `changed` rows flag edits to headline, description, CTA or `landingDomain`, often the first sign of a new promotion.
- **Spot resellers and affiliates on your brand.** Search your own domain to see every advertiser running ads that point to it, including ones you didn't authorize.
- **Agency audits and pitches in minutes.** Export a prospect's full competitor ad library (Search, YouTube, Shopping) to Sheets or Excel, by country (`region`) and date range.
- **Read image-only text ads.** Built-in OCR fills `headline` and `description` from archived screenshots (`textSource: "ocr"`), so text ads aren't blank rows.

Also used by creative strategists (swipe files of YouTube, image and Shopping creatives by country), market-entry research (which advertisers are active in a `region`, on which platforms) and compliance teams (archive what an advertiser showed in a region and period).

### Why use this actor?

| | This actor | Typical Transparency scrapers |
| --- | --- | --- |
| Input | Advertiser **name, domain, ID or URL**, plus free-text search terms. Mix many in one run | Usually one URL or one query |
| Monitoring | Built-in `onlyNewOrChanged` mode with per-field change detection (`changeType`, `changedFields`) | Re-scrape everything and diff it yourself |
| Platforms | Every ad is tagged with the surfaces it ran on, including outside the EU (platform-membership scan) | Platforms often only for EU ads |
| Reliability | Retries with exponential backoff and jitter, proxy session rotation, automatic escalation to residential proxy when rate-limited, partial results instead of crashes | Fails the whole run on rate limits |
| Transparency | `OUTPUT` run summary and `QUALITY_REPORT` with field completeness, speed and estimated cost on every run | No quality metrics |
| Image-only text ads | Built-in OCR reads headline, description and display URL from the archived screenshot, marked `textSource: "ocr"`, cached per creative | Empty fields |
| Cost | Pure HTTP/JSON plus lightweight WASM OCR, 512 MB default (256 MB works), about 1.5 KB of proxy traffic per ad | Often headless browsers |

### What data can I extract?

| Field | Description |
| --- | --- |
| `advertiserId`, `advertiserName`, `advertiserLegalName`, `advertiserCountry`, `advertiserVerified` | Who is paying for the ad |
| `creativeId`, `adDetailUrl` | Stable ad ID and link to the ad on the Transparency Center |
| `format` | `TEXT`, `IMAGE` or `VIDEO` |
| `firstShown`, `lastShown`, `numServedDays` | When the ad ran (Google's timestamps) and how many days Google says it was served |
| `firstSeenDate`, `lastSeenDate`, `daysRunning` | Ad longevity: first and last shown as UTC dates (`YYYY-MM-DD`), and `daysRunning` = days from first to last, counting both days (same day = 1). `null` when Google gives no date; never guessed |
| `regions`, `regionStats[]` | Countries where it ran, with per-country dates, impression ranges and platforms |
| `platforms` | `GOOGLE_SEARCH`, `YOUTUBE`, `GOOGLE_SHOPPING`, `GOOGLE_MAPS`, `GOOGLE_PLAY` |
| `impressionsMin`, `impressionsMax` | Impression range (Google only publishes this for some regions, mainly the EU/EEA) |
| `headline`, `description`, `text`, `callToAction` | Ad copy pulled from the rendered preview, or from the ad image via OCR |
| `textSource`, `ocrConfidence` | `native` (from the preview), `ocr` (read from the ad image) or `null` (no text found); OCR confidence 0–100 |
| `imageUrls`, `videoUrls`, `previewUrl` | Creative assets (YouTube links for video ads) |
| `displayUrl`, `landingUrl`, `landingDomain` | Where the ad points |
| `variantsCount` | Number of creative variations Google shows |
| `query` | Which input and filters produced the row |
| `changeType`, `changedFields` | Monitor mode only: `new`, `changed` or `stopped`, plus which fields changed (`running` when an ad stopped or started again) |
| `raw` | Optional raw API payloads (`includeRawData`) |

### Setup: how to monitor competitor Google ads

1. Click **Try for free** / **Start**.
2. Enter one or more **advertisers** (for example `nike.com`, `Zalando`, `AR15589870916631265281`) or **search terms**.
3. Optionally choose a region, date range, format and platform.
4. Set **Max ads per advertiser** (prefilled 20 for a quick first run; 100 when omitted via API).
5. Run it, then download the data as JSON, CSV or Excel, or connect it to Make, Zapier, n8n, Slack or Google Sheets.

For ongoing competitor tracking, save the input as a **task**, turn on **Only new or changed ads**, and add a **schedule** (daily or weekly).

### Input examples

**All recent ads from a brand's domain**

```json
{
  "advertisers": ["nike.com"],
  "maxAds": 200
}
```

**Video ads on YouTube in Germany in the last 90 days**

```json
{
  "advertisers": ["Duolingo"],
  "region": "DE",
  "format": "VIDEO",
  "platform": "YOUTUBE",
  "dateFrom": "90 days",
  "maxAds": 100
}
```

**Discover advertisers by search term (top 2 matches each)**

```json
{
  "searchTerms": ["adidas", "running shoes"],
  "maxAdvertisersPerSearchTerm": 2,
  "dateFrom": "30 days",
  "maxAds": 50
}
```

**Weekly competitor monitor (only new or changed ads)**

```json
{
  "advertisers": ["competitor-one.com", "Competitor Two GmbH", "AR15589870916631265281"],
  "region": "US",
  "onlyNewOrChanged": true,
  "stateStoreName": "my-competitor-monitor",
  "maxAds": 500
}
```

**Long-running ads only (6+ months): a competitor's proven winners**

```json
{
  "advertisers": ["nike.com"],
  "region": "US",
  "longRunningOnly": true,
  "maxAds": 200
}
```

`longRunningOnly` keeps ads with `daysRunning` of 180 or more. For another bar, set `minDaysRunning` instead (for example `90`); if both are set, the larger one applies. Ads that don't qualify are skipped before their details are fetched where possible, and are never saved or charged. `maxAds` still counts every ad checked, so raise it to find more.

**Fast, low-cost listing (no ad copy, about 1,000 ads per minute)**

```json
{
  "advertisers": ["amazon.com"],
  "includeCreativeDetails": false,
  "maxAds": 1000
}
```

Dates accept absolute values (`2026-01-31`) or relative ones (`30 days`, `2 weeks`, `today`).

### Text from image-only ads (OCR)

For many text ads (especially older and EU ones), Google keeps only a screenshot of the ad. With `extractTextFromImages` on (the default), the actor reads the **headline, description and display URL** from that screenshot using Tesseract (WASM, no external service, no extra proxy traffic: images are downloaded directly).

- It runs only for `TEXT` ads whose preview has no text and that have an archived image, and only when `includeCreativeDetails` is on. Ads with native text are never OCR'd.
- The layout is read from the screenshot's colours: the blue line is the headline, the gray block under it is the description, the URL line above the headline is the display URL. Sitelinks, ratings and "App installed" rows are left out.
- Fields read this way carry `textSource: "ocr"` and `ocrConfidence`. The full list of recognised lines goes into `text`.
- Results are cached per creative in the `google-ads-transparency-cache` key-value store, so a monitor rerun (or any later run) never OCRs the same image twice. OCR text isn't used for change detection; a new image shows up as a `media` change.
- `ocrLanguages` picks the models. The default is English, German, French, Spanish, Italian, Dutch, Portuguese and Polish (bundled). You can add others (Nordic, Czech, Greek, Turkish, …); those are downloaded at run start.
- OCR is CPU-bound. At 512 MB it takes about 5–9 s per image and at 2,048 MB about 1.4 s, at roughly the same cost per image. For large OCR-heavy pulls, raise the memory to go faster. Below 512 MB the actor limits OCR to 3 languages.
- Turn it off (`extractTextFromImages: false`) when you only need ad metadata or want maximum speed.

On our test sample of 20 OCR'd ads, 16 of 20 headlines and 15 of 20 descriptions came out exactly right. Most of the remaining errors were one to four wrong accented characters (for example `ß`, `å`, `ł`). One ad had a product photo covering the text. All 20 display URLs were correct.

### Output sample (one full JSON record)

```json
{
  "advertiserId": "AR18378488041124659201",
  "advertiserName": "Nike Retail BV",
  "advertiserLegalName": "Nike Retail BV",
  "advertiserCountry": "NL",
  "advertiserVerified": true,
  "creativeId": "CR11779417970759958529",
  "format": "TEXT",
  "firstShown": "2025-10-24T16:24:40.000Z",
  "lastShown": "2026-10-09T13:37:11.000Z",
  "numServedDays": 351,
  "firstSeenDate": "2025-10-24",
  "lastSeenDate": "2026-10-09",
  "daysRunning": 351,
  "regions": ["AT", "DE", "LU"],
  "regionStats": [
    {
      "regionCode": "AT",
      "regionName": "Austria",
      "firstShown": "2025-10-25",
      "lastShown": "2026-10-06",
      "impressionsMin": 0,
      "impressionsMax": 1000,
      "platforms": [{ "platform": "GOOGLE_SEARCH", "impressionsMin": 0, "impressionsMax": 1000 }]
    }
  ],
  "platforms": ["GOOGLE_MAPS", "GOOGLE_SEARCH"],
  "impressionsMin": 10000,
  "impressionsMax": 15000,
  "headline": "Aktuelle Workout-Schuhe",
  "description": "Entdecke {Keyword:Nike schuhe} online auf Nike.com – der offiziellen Nike Website.",
  "callToAction": null,
  "textSource": "native",
  "ocrConfidence": null,
  "imageUrls": [],
  "videoUrls": [],
  "displayUrl": "nike.com",
  "landingUrl": null,
  "landingDomain": "nike.com",
  "variantsCount": 1,
  "adDetailUrl": "https://adstransparency.google.com/advertiser/AR18378488041124659201/creative/CR11779417970759958529?region=anywhere",
  "query": { "input": "nike.com", "type": "domain", "region": "anywhere", "format": "ALL", "platform": "ALL" },
  "detailsIncluded": true,
  "scrapedAt": "2026-10-09T13:54:51.775Z"
}
```

The real `previewUrl` and `text` fields are included as well; they're left out here to keep the sample short. Rows produced in monitor mode also carry `"changeType": "new" | "changed" | "stopped"` and `"changedFields": ["regions", "headline", …]`.

#### Key-value store records

- `OUTPUT`: run summary with status (`SUCCEEDED`, `PARTIAL` or `FAILED`), each query's resolution and ad counts, monitor counts (new, changed, unchanged), charging (`payPerEvent`, `chargedEvents`, `chargeLimitReached`), HTTP statistics, errors and estimated cost.
- `QUALITY_REPORT`: per-field completeness (overall and by format), ads per minute, proxy traffic, error categories and any `feedback` you sent with the run.

### Alerts: get new competitor ads in Slack, email or a sheet

The actor finds the changes; Apify delivers them. A typical PPC alert setup:

1. Save your input as a **task** with `onlyNewOrChanged: true` and a fixed `stateStoreName` (for example `competitors-us`). Use one state store per monitor so tasks don't overwrite each other's snapshots.
2. Add a **schedule** to the task (weekly on Monday morning, or daily for fast-moving competitors).
3. On the task's **Integrations** tab, connect **Slack**, **email**, **Google Sheets**, **Zapier**, **Make** or **n8n**, or add an Apify **webhook** on *Run succeeded* that points at your endpoint. Each run's dataset contains only `new`, `changed` and `stopped` ads, so the alert is the change list, not the whole library.
4. Optional: set `reportIssueWebhook` to get a POST when a run has errors, ends `PARTIAL`, or carries `feedback`, so a broken monitor never fails silently.

The first monitor run outputs the full baseline (every current ad as `new`) unless you turn off `emitBaselineOnFirstRun`. With it off, the first run only saves the snapshot and outputs (and charges) nothing. Monitor state is stored per query, so several tasks can share one store or stay separate.

### How much does it cost?

The actor is pure HTTP plus an optional OCR step and runs at 512 MB by default. On our platform test runs, compute plus residential-proxy usage came to roughly **$0.04–0.07 per 1,000 ads with full details and OCR off**, and about **$0.007 per 1,000 ads** in list-only mode (`includeCreativeDetails: false`). OCR adds about **$0.18–0.30 per 1,000 ads that actually need OCR**, so the total depends on the advertiser. On nike.com (25% of ads needed OCR) it was $0.13 per 1,000 ads, and on Zalando DE (97% image-only) it was $0.23 per 1,000. These are platform usage figures from our tests, not the price.

#### Pricing

Pay-per-event, one event that matters:

| Event | Price | When it's charged |
| --- | --- | --- |
| `ad` | **$0.0017 per ad** ($1.70 per 1,000 ads) | One ad creative saved to the dataset |
| `apify-actor-start` | $0.00005 per run | Apify's standard run-start event (covers the first seconds of compute) |

The Pricing tab on the Store page is always the authoritative price.

- You pay only for ads saved to the dataset. The `OUTPUT` summary and `QUALITY_REPORT` records are free.
- In monitor mode (`onlyNewOrChanged`) only new or changed ads are saved, so unchanged ads cost nothing. Example: a weekly monitor of 5 competitors that finds 40 new or changed ads costs about $0.07 per run.
- A one-off audit pull of 1,000 ads costs about $1.70.
- Set **Max cost per run** in the run options to cap your spend. When the cap is reached, the actor stops cleanly, keeps what it has saved, and marks `charging.chargeLimitReached: true` in `OUTPUT`. In monitor mode, ads it didn't get to are still reported on the next run.

### Proxy

By default the actor uses Apify **Residential** proxy. Only small JSON responses go through it, so traffic is about 1.5 KB per ad, and ad previews are fetched directly. Google rate-limits datacenter IPs aggressively. If you choose datacenter or no proxy and the actor hits repeated rate limits, it switches to residential automatically (`proxyFallbackToResidential`, default on).

### FAQ

**Is this a Google ad spy tool / ad library like the Meta Ad Library?** Yes, for Google. It exports the same public data as the Google Ads Transparency Center, in bulk and in a structured format, so you can research competitors' ads across Search, YouTube, Shopping, Maps and Play.

**Does it show ad spend, keywords or CTR?** No. Google doesn't publish spend, targeted keywords or performance. You get the creatives, copy, dates, regions, platforms and (where Google publishes them) impression ranges.

**What does `textSource` mean?** `native` means the text came from Google's rendered ad preview, `ocr` means it was read from the archived ad image (with `ocrConfidence`), and `null` means no text was available.

**Can I export to CSV or Google Sheets?** Yes. Every Apify dataset downloads as JSON, CSV, Excel or XML, and integrations push it to Google Sheets, Slack, Make, Zapier or n8n.

**Can I search by keyword, like "running shoes"?** The Transparency Center only searches advertiser names and domains, not ad text. `searchTerms` returns the best-matching advertisers (and domains). For brand terms this works well. A generic phrase may match a small advertiser whose name contains those words.

**Why are some text-ad headlines empty?** For many older or EU text ads, Google only stores an image snapshot of the ad. With OCR on (default), the actor reads the text from that image (`textSource: "ocr"`). Headlines stay `null` when the ad has no image to read, or when the headline is dynamic (keyword insertion or local ads) and Google stores only a placeholder. The actor doesn't make up a headline in those cases.

**Why are impressions missing?** Google publishes impression ranges only for some regions (mostly the EU/EEA) and for political ads.

**Is there a "Display" platform filter?** No. The Transparency Center's platform filter only offers Search, YouTube, Shopping, Maps and Play, so the actor offers the same five.

**Does it detect removed ads?** Yes, in monitor mode. An ad is saved once as `changeType: "stopped"` when its `lastShown` date is at least `stoppedAfterDays` days old (default 3) and didn't advance since the previous run, or when it is missing from a complete listing two runs in a row (listings cut short by `maxAds`, an error or a cost limit never count). A stopped ad that runs again comes back as `changed` with `changedFields: ["running"]`. Turn this off with `reportStoppedAds: false`.

**How is `daysRunning` counted?** It's `lastSeenDate` minus `firstSeenDate` plus 1, so both the first and the last day count (an ad shown on one day only has `daysRunning: 1`). The dates are the UTC calendar dates of Google's `firstShown` and `lastShown`. It's the calendar span, not `numServedDays` (the days Google says the ad was actually served): an ad that paused for a while has a lower `numServedDays`. Google counts days in its own time zone, so the two can also differ by a day. If either date is missing, `daysRunning` is `null`; with `minDaysRunning` or `longRunningOnly` on, those ads are left out. Our Meta Ad Library monitor uses the same three fields and the same definition.

**Does the long-running filter work in monitor mode?** Yes. Only ads that pass the filter start being tracked, so an ad shows up as `new` the run it first reaches the bar. Ads already tracked stay in, so their changes and their stop are still reported. Turning the filter on starts its own baseline (it doesn't reuse the snapshot of the same query without the filter). `stopped` rows for tracked ads are always reported; they carry `lastSeenDate` but `firstSeenDate`/`daysRunning` are `null` because the snapshot doesn't keep the first shown date.

**What happens if an advertiser doesn't exist?** That query is reported as `NOT_FOUND` in `OUTPUT`, and the other queries still run. If every query fails, the run fails with a clear message.

**How do I report a problem?** Use the Issues tab, or put a note in the `feedback` input field. It's saved to `QUALITY_REPORT` and reviewed weekly. You can also set `reportIssueWebhook` to get a POST whenever a run has errors.

### Limits

- Only public Transparency Center data: no spend, keywords, audiences or click data.
- Google may rate-limit heavy usage; the actor retries and falls back to residential proxy, but very large pulls can end as `PARTIAL`.
- The actor paginates (40 ads per page) until `maxAds` is reached or there are no more pages. `maxAds` applies to each advertiser input, domain, or resolved search-term advertiser. With `minDaysRunning` / `longRunningOnly`, `maxAds` counts ads checked, not ads saved.
- OCR text can contain small character errors, mostly accented letters in languages outside `ocrLanguages`, and text covered by a product image. Check `ocrConfidence` and `textSource`.
- `landingUrl` is rarely exposed by Google. `landingDomain` comes from the display URL or the preview.
- Date filters cover what the Transparency Center exposes (the actor searches from 2018-01-01 onward when no start date is set).
- With details on and OCR off, our tests ran at about 100–170 ads per minute. OCR is CPU-bound: at 512 MB, expect roughly 10–25 ads per minute when most ads are image-only, or about 35 per minute at 2,048 MB. For very large pulls, use list-only mode first.

### Legal

This actor only collects data that Google publishes in the public Ads Transparency Center, without logging in. It doesn't collect personal data about individuals. Still, the results can include advertiser names and other information. Make sure your use complies with Google's Terms of Service and the laws that apply to you (for example GDPR). If you're unsure, consult your lawyer. You can read more in Apify's blog post on [the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/).

### Support

Found a bug or need a field? Open an issue on the Issues tab. We review issues, feedback and quality reports every week.

Built and maintained by **SingletonOps**, a brand of Vibex LLC. Also from SingletonOps: Meta Ad Library Monitor (new competitor Facebook and Instagram ads), RSS Change Monitor and Sitemap Change Monitor.

# Actor input Schema

## `advertisers` (type: `array`):

One advertiser per line. Accepts an <b>advertiser name</b> (e.g. <code>Nike</code>), a <b>domain</b> (e.g. <code>nike.com</code> — returns every advertiser running ads for that site), an <b>advertiser ID</b> (e.g. <code>AR16735076323512287233</code>), or a pasted <b>Transparency Center URL</b> (advertiser or domain page). Names are resolved to the best-matching advertiser with the most ads; use the ID for an exact match.

## `searchTerms` (type: `array`):

Keywords to look up in the Transparency Center search box (e.g. <code>running shoes</code>, <code>insurance</code>). Each term is resolved to the top matching advertisers (see the next field) and their ads are scraped. Note: Google's Transparency Center searches advertiser names and domains, not ad text.

## `maxAdvertisersPerSearchTerm` (type: `integer`):

How many top-matching advertisers to scrape for each search term.

## `maxAds` (type: `integer`):

Maximum number of ads to scrape for each advertiser, domain, or resolved search-term advertiser. In monitor mode, unchanged ads are checked but not saved.

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

Only ads shown in this country. Choose <b>Anywhere</b> for all regions.

## `dateFrom` (type: `string`):

Only ads shown on or after this date. Absolute (YYYY-MM-DD) or relative (e.g. 30 days = last 30 days). Leave empty for no limit.

## `dateTo` (type: `string`):

Only ads shown on or before this date (inclusive). Leave empty for today.

## `format` (type: `string`):

Only ads of this format.

## `platform` (type: `string`):

Only ads shown on this Google platform. (The Transparency Center has no separate Display filter; Display ads appear under All platforms.)

## `longRunningOnly` (type: `boolean`):

Keep only ads that have been running for at least 180 days (<code>daysRunning</code> ≥ 180), the "proven winners" a competitor keeps paying for. Same as <b>Minimum days running</b> = 180; if both are set, the larger value wins. Ads without a Google first/last shown date are dropped while this is on. Filtered-out ads are not saved or charged; <b>Max ads</b> still counts every ad checked, so raise it to find more long-running ads.

## `minDaysRunning` (type: `integer`):

Keep only ads whose <code>daysRunning</code> (last shown date minus first shown date, counting both days) is at least this many days, e.g. 90 or 180. 0 = off (default). Applied after the dates are read, so it works with every other filter. Ads without dates are dropped while the filter is on.

## `includeCreativeDetails` (type: `boolean`):

Fetch each ad's detail and preview: headline, description, call to action, display URL / landing domain, image and YouTube video URLs, every region with first/last shown dates, platforms, impression ranges (where Google publishes them, e.g. EU) and number of variants. Turn off for a ~3-5x faster, cheaper list of ads (IDs, format, dates, preview image).

## `extractTextFromImages` (type: `boolean`):

Many text ads are archived by Google only as a screenshot. When on, the actor reads the headline, description and display URL from that image with OCR (only for TEXT ads that have no text in their preview; requires creative details). OCR fields are marked with textSource = "ocr". Results are cached per creative, so monitor reruns never redo OCR.

## `ocrLanguages` (type: `array`):

Languages the OCR model should recognise. More languages = better accents/umlauts across markets but slightly more memory and time. Default: English, German, French, Spanish, Italian, Dutch, Portuguese, Polish.

## `detectPlatforms` (type: `boolean`):

Google publishes per-platform stats only for some regions (mostly the EU). With this on, the Actor also lists each advertiser once per platform filter (Search, YouTube, Shopping, Maps, Play) and tags every ad with the platforms it appears on. Adds a few cheap list requests per query; only used when Platform = All and creative details are on.

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

Adds a <code>raw</code> field with the unprocessed Transparency Center responses. Useful for debugging or custom parsing; makes items much larger.

## `onlyNewOrChanged` (type: `boolean`):

Remembers what each query returned last time (in a named key-value store) and saves only ads that are <b>new</b>, whose content/reach <b>changed</b>, or that <b>stopped</b> running. Every item gets <code>changeType</code> and <code>changedFields</code>. Schedule the Actor daily or weekly to get competitor-ad alerts. Keep filters identical between runs.

## `emitBaselineOnFirstRun` (type: `boolean`):

On the first monitoring run every ad is new. Turn off to just record a silent baseline and only get truly new/changed ads from the next run on.

## `reportStoppedAds` (type: `boolean`):

Monitoring mode only. Saves an ad once with <code>changeType: "stopped"</code> when its <code>lastShown</code> date stops advancing (see the next setting), or when it disappears from a complete listing for 2 runs in a row. If it runs again later it comes back as <code>changed</code> with <code>changedFields: \["running"]</code>. Stopped rows are saved ads like any other.

## `stoppedAfterDays` (type: `integer`):

Monitoring mode only. An ad whose last shown date is at least this many days old (and did not advance since the previous run) is reported as stopped. Google updates last shown dates with some delay, so values under 2 can cause noise.

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

Named key-value store that keeps monitoring snapshots between runs. Use different names to keep independent monitors (e.g. one per client).

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

Google aggressively rate-limits datacenter IPs on the Transparency Center. The default <b>Apify Residential</b> proxy is the most reliable and, because only small JSON API responses go through it (ad previews are fetched directly), it adds only about one or two cents per 1,000 ads. Datacenter or your own proxies work too — see the fallback option below.

## `proxyFallbackToResidential` (type: `boolean`):

If the selected proxy (or no proxy) keeps getting rate-limited by Google, switch to Apify Residential proxy for the rest of the run instead of returning ads without details. Has no effect when Residential is already selected.

## `maxConcurrency` (type: `integer`):

Parallel detail requests. Higher is faster but more likely to be rate-limited.

## `maxRetries` (type: `integer`):

Retries with exponential backoff and proxy-session rotation on rate limits, timeouts and server errors.

## `requestTimeoutSecs` (type: `integer`):

Timeout for each HTTP request.

## `feedback` (type: `string`):

Anything missing or wrong? Write it here. It is stored in this run's QUALITY_REPORT, which the maintainers review weekly to improve the Actor. Please don't include personal data.

## `reportIssueWebhook` (type: `string`):

If set, the Actor POSTs a JSON report (run summary + QUALITY_REPORT + your feedback) to this URL when the run has errors, partial results, or feedback. Handy for Slack/Zapier/Make alerts.

## Actor input object example

```json
{
  "advertisers": [
    "nike.com"
  ],
  "searchTerms": [],
  "maxAdvertisersPerSearchTerm": 3,
  "maxAds": 20,
  "region": "US",
  "dateFrom": "30 days",
  "format": "ALL",
  "platform": "ALL",
  "longRunningOnly": false,
  "minDaysRunning": 0,
  "includeCreativeDetails": true,
  "extractTextFromImages": true,
  "ocrLanguages": [
    "eng",
    "deu",
    "fra",
    "spa",
    "ita",
    "nld",
    "por",
    "pol"
  ],
  "detectPlatforms": true,
  "includeRawData": false,
  "onlyNewOrChanged": false,
  "emitBaselineOnFirstRun": true,
  "reportStoppedAds": true,
  "stoppedAfterDays": 3,
  "stateStoreName": "google-ads-transparency-monitor",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "proxyFallbackToResidential": true,
  "maxConcurrency": 4,
  "maxRetries": 6,
  "requestTimeoutSecs": 30
}
```

# Actor output Schema

## `ads` (type: `string`):

Dataset with one item per ad creative (advertiser, format, dates, regions, platforms, copy, media, links).

## `longRunningAds` (type: `string`):

Same dataset in the "Long-running ads" view: first/last seen dates and daysRunning per ad.

## `runSummary` (type: `string`):

Counts per query, monitoring new/changed/unchanged/stopped totals, errors by category and timing.

## `qualityReport` (type: `string`):

Field completeness %, empty-field counts, error categories, speed and cost-per-1,000-ads estimate.

# 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 = {
    "advertisers": [
        "nike.com"
    ],
    "searchTerms": [],
    "maxAds": 20,
    "region": "US",
    "dateFrom": "30 days",
    "includeCreativeDetails": true,
    "extractTextFromImages": true,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("singletonops/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 = {
    "advertisers": ["nike.com"],
    "searchTerms": [],
    "maxAds": 20,
    "region": "US",
    "dateFrom": "30 days",
    "includeCreativeDetails": True,
    "extractTextFromImages": True,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("singletonops/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 '{
  "advertisers": [
    "nike.com"
  ],
  "searchTerms": [],
  "maxAds": 20,
  "region": "US",
  "dateFrom": "30 days",
  "includeCreativeDetails": true,
  "extractTextFromImages": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call singletonops/google-ads-transparency-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,singletonops/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/3W51HRjN938zr6he6/builds/zt8FwK0vhgXVyTQpb/openapi.json
