# Competitor Ad Monitor: Google Ads Transparency & Domain Check (`solidfetch/google-ads-transparency-scraper`) Actor

Competitor ad monitor for Google Ads: schedule it daily or weekly and get only the new ads, with an alert through Apify integrations. Bulk check domains running Google ads: one row per domain, yes or no, with its advertisers. By brand, domain or advertiser ID: dates, days shown, EU reach.

- **URL**: https://apify.com/solidfetch/google-ads-transparency-scraper.md
- **Developed by:** [Solid Fetch](https://apify.com/solidfetch) (community)
- **Categories:** Lead generation, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 ad scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Competitor Ad Monitor for Google Ads: Transparency Scraper and Domain Check

Scrape the Google Ads Transparency Center (Google's public ad library) and get every ad a competitor runs, one row per ad. Search by brand name, domain or advertiser ID, then filter by country, date, format and platform (Google Search, YouTube, Shopping, Maps, Play). Each row has the advertiser, the format, the first and last shown dates, the days shown and a link to the ad. Turn on `adDetails` for ads shown in the EU and each row also gets the impression range and the audience targeting the advertiser chose. No login and no browser. Use it as a scraper from the Console, as an API, or from an AI agent through the Apify MCP server.

### What you get for your money

- **The brand you asked for, not its namesakes.** A name search resolves to the brand's own advertiser account (for Allegro, Allegro sp. z o. o.), and switches to the brand's domain when that has many more ads, as with Temu or Shein. Unrelated companies that share the name stay out of your results and your bill.
- **`maxAds` counts per brand you type.** Ask for 50 Allegro ads and you get at most 50, even if Google lists several Allegro-like accounts.
- **Reach and targeting at the same price per ad.** With `adDetails` on, EU ads get impression ranges, targeting and per-country dates. It costs no extra per ad; the run takes a few seconds longer.
- **Days served as Google reports them**, so the number matches what you see in the Transparency Center.

### What people use it for

- **Competitor research.** Put in the brands you compete with and see which ads they ran, in which format and for how long.
- **Video ads.** With the Video format, most video ads come with their YouTube link and thumbnail.
- **One platform only.** Set `platform` to `YOUTUBE`, `SEARCH`, `SHOPPING`, `MAPS` or `PLAY` to get only the ads Google showed there, for example a competitor's YouTube ads or their Shopping ads.
- **Shopping prices.** For shopping ads that carry product data, the actor returns the product title, price, currency and seller. Turn on `onlyWithProducts` to keep only those ads.
- **Who advertises for a domain.** Search by a domain to see the advertisers whose ads send traffic to it.
- **Market scans by country.** Filter by country and dates, for example the last 30 days in Poland.
- **New competitor ads every day.** Turn on `onlyNewSinceLastRun` and schedule the run: each run returns only the ads that appeared since the run before.
- **Reach and targeting of ads shown in the EU.** Turn on `adDetails` to get the impression range of each ad, the audience targeting categories the advertiser used, and first and last shown dates per country.

### How to use it

1. Type one or more brand names, domains or advertiser IDs. Advertiser page URLs from the Transparency Center work as IDs.
2. Optionally pick a country, a date range or a number of recent days, a format (text, image, video) and a platform (Google Search, YouTube, Shopping, Maps, Play).
3. Run it and download the results as JSON, CSV or Excel, or read them through the API.

```json
{
    "advertiserNames": ["Nike", "Allegro", "Temu"],
    "domains": ["zalando.pl"],
    "region": "Poland",
    "shownInLastDays": 30,
    "maxAds": 200
}
```

Each name, each domain and the list of advertiser IDs is a separate search. `maxAds` applies to each search, and one run takes up to 500 searches. An ad found by two searches is saved and charged once, and its `searchQuery` field names the search that found it first.

If a name matches no advertiser, the run goes on with the other searches and lists the name in its status message. If no name in the run matches, the run still finishes successfully with no ads and suggests close advertiser names that Google knows, for example `No advertiser matched "Adiddas". Did you mean: adidas?`

#### How brand names are matched

Google suggests advertisers for a name. The actor picks the advertiser whose name starts with your word and has the most ads, and it prefers an advertiser from the country you selected. The log lists the other matching advertisers, and you can pass their IDs to get them too.

Some brands run their ads under another company name. Temu has no advertiser called Temu: in a sample of 100 ads that lead to temu.com, 91 came from ELEMENTARY INNOVATION PTE. LTD. and 9 from Whaleco Inc. When no advertiser carries the name, or when the best one is a small namesake with fewer than 1,000 ads and more ads lead to the brand's domain, the actor searches by that domain instead. A domain search returns every advertiser whose ads lead to the domain, so it can include resellers and app publishers. The fields `resolvedBy` and `matchedAdvertiser` show which way each search went. For one exact advertiser, use its ID.

### Monitor new ads

Turn on `onlyNewSinceLastRun` and each run returns only the ads that the same search has not returned before and that started running since the last run. Schedule it and it works as a daily alert for new competitor ads.

```json
{
    "advertiserNames": ["Nike", "Adidas"],
    "region": "Poland",
    "maxAds": 300,
    "onlyNewSinceLastRun": true
}
```

- **The first run is the baseline.** No earlier state exists, so it returns every matching ad, and the log and the run's status message say it is the baseline. From the second run on, only new ads are saved.
- **You pay only for new ads.** Ads seen by earlier runs are skipped before saving and are not charged. A run with nothing new costs only the start fee.
- **What counts as new.** An ad is new when it is not in the list of seen ads and its first-shown date is not earlier than 2 days before the start of the previous run. An ad that is not in the list but started long before is an old ad that this run happened to read for the first time, because Google returned a different slice of a large advertiser. It is added to the list, not saved and not charged. The log and the status message say how many were skipped this way. An ad without a first-shown date counts as new. The first run has no earlier run to compare with and returns every ad, and so does the run after a first run that stopped at the maximum cost. A later run that stops at the maximum cost, or that searched nothing because no input value could be used, leaves the comparison point where it was, so the ads it did not get to stay new.
- **How deep each run looks, and what it can miss.** Google lists the ads by last-shown date, newest first, not by first-shown date, and every ad that runs today has today as its last-shown date. `maxAds` is how many ads each search reads. For a brand that runs more ads at once than `maxAds`, which ones you get among the running ads is up to Google, so a new ad can fall outside the ads read that day and show up in a later run, as long as it started within the last 2 days before that run, or not at all. Set `maxAds` above the number of ads the brand runs at the same time when you can; for brands with hundreds of thousands of ads, use a country, format or platform filter to narrow the search.
- **Older lists keep working.** A list saved before this rule existed has no start time of its own, so the first run after the update takes the day the list was last saved as the previous run.
- **One list per search.** The list of seen ads belongs to the names, domains, IDs, country, format and platform. Changing `maxAds`, dates or `adDetails` keeps the same list, changing the country or a name starts a new baseline. Give `monitorName` a name to choose the list yourself, for example to share it between two tasks.
- **Still running ads.** With `includeLastShownChanges`, the run also saves ads seen before whose last-shown date moved since the last run.
- Each new row has `changeType` (`new` or `lastShownChanged`) and `previousLastShown`.
- If a run stops at the maximum cost per run, the ads it did not save stay new for the next run.
- The list is kept in the key-value store `google-ads-transparency-state` on your account and holds up to 50,000 ads per name, so `maxAds` times the number of searches may be at most 50,000. When the list is full, the ads seen longest ago are forgotten and count as new if they come back. Two runs with the same name at the same time do not lose each other's ads.

To schedule it in Apify Console:

1. Fill in the input above and click **Save as a new task**.
2. Go to **Schedules** in the left menu, create a schedule with a time, for example every day at 8:00, and add the task to it.
3. Run the task once by hand to save the baseline.
4. To get the new ads by email, Slack or a webhook, add an integration on the task for "Run succeeded".

### Check which domains advertise

Set `mode` to `domainCheck` to answer one question for a list of domains: does this domain advertise on Google? Each domain gets one row from one small request to Google, with no preview or detail requests.

```json
{
    "mode": "domainCheck",
    "domains": ["nike.com", "https://www.zalando.de/damen/", "example-shop.pl"],
    "region": "Poland"
}
```

A row for nike.com, built from a recorded Google response:

```json
{
    "domain": "nike.com",
    "input": "https://www.nike.com/pl/",
    "hasAds": true,
    "advertisers": [
        { "id": "AR16735076323512287233", "name": "Nike, Inc." },
        { "id": "AR18378488041124659201", "name": "Nike Retail BV" }
    ],
    "adCountEstimate": { "min": 100000, "max": 200000 },
    "latestShown": "2026-10-03T08:58:03.000Z",
    "formats": ["TEXT"],
    "sampleFormatCounts": { "text": 5, "image": 0, "video": 0 },
    "sampleSize": 5,
    "likelyOwner": { "id": "AR16735076323512287233", "name": "Nike, Inc.", "ids": ["AR16735076323512287233"] },
    "ownerMatch": "name-match",
    "otherAdvertisers": [{ "id": "AR18378488041124659201", "ids": ["AR18378488041124659201"], "name": "Nike Retail BV", "nameMatch": true }],
    "activeLast30Days": true,
    "daysSinceLastAd": 10,
    "adsLast30Days": 5,
    "adsLast30DaysIsSample": true,
    "firstSeen": "2023-11-16T22:23:39.000Z",
    "checkedAt": "2026-10-13T12:00:00.000Z",
    "region": null,
    "error": null
}
```

- **`input`** is the value exactly as you sent it (`https://www.nike.com/pl/`), so you can map a row back to your list. `domain` is the cleaned host.
- **`hasAds`** is `true` when the Transparency Center lists at least one ad that leads to the domain, in the chosen country if you set `region`. It is not limited to ads running today: look at `latestShown` to see how recent they are.
- **`advertisers`** lists up to 5 advertisers from the first 20 ads Google shows for the domain, with their IDs, so you can pass an ID to the ads mode to get that advertiser's ads.
- **`adCountEstimate`** is the rounded total Google reports for the domain, as a range. It is `{ "min": 0, "max": 0 }` for a domain without ads.
- **`formats`** are the formats seen among those first 20 ads (`TEXT`, `IMAGE`, `VIDEO`), not across every ad of the domain.
- **`likelyOwner`** and **`ownerMatch`** say who probably owns the domain, from the same first 20 ads. `name-match`: the advertiser's name starts with the domain's brand or has it as a word (for nike.com, Nike Retail BV; for booking.com, Booking.com B.V.), and with several such companies the one with the most ads on the page wins, except that a branch (a name with Podruznica, Branch, Filiale, Sucursal and the like, as in "Samsung Electronics Austria GmbH Podruznica Zagreb") loses to any other company of the brand. Advertiser IDs that carry the same name (booking.com, mcdonalds.com and hm.com each have one company under two IDs) count as one company, and `likelyOwner.ids` lists all of its IDs, the one with the most ads first (`id`). `largest-advertiser`: no name fits, so it is the advertiser with the most ads on the page, which is a guess. Both are `null` for a domain without ads. It is a guess from names and a 20-ad sample, not a verified owner, and a brand whose company has a different name (Temu, run by Whaleco) gets `largest-advertiser`.
- **`otherAdvertisers`** lists up to 5 more companies from the page, one entry per name with all its advertiser IDs in `ids` (`id` is the one with the most ads), each with `nameMatch`. `false` means the name does not fit the brand, which is how agencies and resellers (for nike.com, WPP and M Media Group) show up; `true` is another company of the brand.
- **`activeLast30Days`** is `true` when `latestShown` is at most 30 days before `checkedAt`, and `false` for a domain with no ads. **`daysSinceLastAd`** is the whole UTC days from `latestShown` to `checkedAt`, or `null` without ads. Both rest on the latest last-shown date among the first 20 ads, which Google lists newest first.
- **`sampleFormatCounts`** counts the ads on the fetched page as `text`, `image` and `video`, and **`sampleSize`** is how many ads that is (at most 20); ads of an unknown format are in none. It is a sample, not the domain's totals: Google's answer carries no per-format totals, and getting them would take extra requests per domain. Do not read it as a distribution of the domain's 80,000 ads. (This field was called `formatCounts` until 21 October 2026; it was one day old, and the old name suggested totals, so it was renamed without an alias. Update a saved view or Clay column that used it.)
- **`adsLast30Days`** is how many of the fetched ads were last shown within 30 days of `checkedAt`. **`adsLast30DaysIsSample`** is `true` when Google has more ads than the page held, so the number is a lower bound; `false` means the page held every ad. **`firstSeen`** is the earliest first-shown date among the fetched ads, not necessarily the domain's first ad. All three come from the page already fetched, with no extra request.
- A domain that could not be checked has `null` in all the sample and activity fields, `likelyOwner`, `ownerMatch`, `activeLast30Days` and `daysSinceLastAd`.
- Up to 1,000 domains per run. Full URLs work: the scheme, path and `www.` are removed, international names are saved as punycode (`żółć.pl` becomes `xn--kda4b0koi.pl`, however you write it), and a domain listed twice is checked once, with the `input` of its first mention.
- **Not a domain, not checked, not charged.** IP addresses, e-mail addresses (`mailto:foo@bar.com`), empty strings and names without a public suffix (`localhost`, `printer.local`) each get a row with `hasAds: null` and the reason in `error`. The check looks at the last label (a plain top-level domain such as `.com`, `.pl` or a punycode one), it does not use the full public suffix list.
- **You pay for each answer**, including `hasAds: false`. A domain that could not be checked (Google kept failing after the retries, or the value is not a domain) is saved with `hasAds: null` and an `error`, and is not charged. One failing domain does not stop the run; after 10 failed domains in a row the run stops, saves the rest as not checked and fails, since Google is then blocking it.
- Two domains are checked at the same time. The run goes back to one request at a time after any failed domain or any CAPTCHA/429 from Google, and rows are always saved in input order.
- Only `domains` and `region` are used in this mode. The ads-mode filters, `maxAds` and `onlyNewSinceLastRun` are ignored, and the log names the ones you set.

### Look up advertisers

Set `mode` to `advertisers` to find the advertiser accounts behind brand names and domains, with their IDs, before you scrape their ads or to map a list of brands to Transparency Center accounts. Each advertiser is one row.

```json
{
    "mode": "advertisers",
    "advertiserNames": ["Nike", "Allegro"],
    "domains": ["zalando.de"],
    "region": "Poland"
}
```

The first row for Nike, built from a recorded Google response:

```json
{
    "query": "Nike",
    "queryType": "name",
    "advertiserId": "AR18378488041124659201",
    "name": "Nike Retail BV",
    "country": "NL",
    "adCountEstimate": { "min": 80000, "max": 90000 },
    "isBestMatch": true,
    "transparencyUrl": "https://adstransparency.google.com/advertiser/AR18378488041124659201",
    "region": null,
    "checkedAt": "2026-10-13T12:00:00.000Z",
    "error": null
}
```

- **A name** returns every advertiser Google suggests whose name contains that name as a whole word: Nike finds Nike Retail BV, Nike, Inc. and NIKE SRL, but not Nikesh. The best match comes first, then the others by ad count.
- **`isBestMatch`** marks the account the ads mode searches for that name (see [How brand names are matched](#how-brand-names-are-matched)). When the ads mode would search the brand's domain instead, as for Shein or Temu, no row of that name is marked. Domains never have a best match.
- **A domain** returns the advertisers on the first 40 ads that lead to it, in the chosen country if you set `region`. Their `country` is `null`, since Google lists it only with name suggestions.
- **`country`** is the country of the advertiser account as Google lists it. **`adCountEstimate`** is Google's rounded count of the advertiser's ads in all countries, as a range.
- An advertiser found by two names or domains is saved and charged once, under the first one that found it.
- Up to 500 names and domains per run. Duplicates are looked up once, and full URLs work for domains.
- **Advertiser lookups are free for now;** a per-advertiser price will be announced on the Pricing tab two weeks ahead. A name or domain without advertisers is saved once with `advertiserId: null` and the reason in `error`, and is not charged. A lookup that could not be done (Google kept failing after the retries, or the value is not a name or a domain) is saved the same way, free. After 10 failed lookups in a row the run stops, saves the rest as not looked up and fails, since Google is then blocking it.
- Only `advertiserNames`, `domains` and `region` are used in this mode. The other filters are ignored, and the log names the ones you set.

### Input

| Field | Description |
|---|---|
| `mode` | `ads` (default) returns ads. `domainCheck` returns one row per domain in `domains` saying whether it advertises. See [Check which domains advertise](#check-which-domains-advertise). `advertisers` returns one row per advertiser account matching `advertiserNames` or running ads for `domains`. See [Look up advertisers](#look-up-advertisers). |
| `advertiserNames` | Brand or advertiser names, one search each. Names that differ only in case or punctuation (`Nike`, `nike`, `NIKE `) are searched once, and the log says so. |
| `advertiserNamesText` | The same names as one text, one per line (or separated by commas when there is no line break). For tools that send a column as one value, see [Use it from Clay, Google Sheets or Make](#use-it-from-clay-google-sheets-or-make). |
| `advertiserIds` | Advertiser IDs (`AR` followed by 20 digits) or advertiser page URLs. All IDs are searched together. An ID that is not valid does not stop the run: it is saved as a free row with `searchQuery` (your value) and `error`, and the valid IDs are searched. |
| `startUrls` | Links copied from adstransparency.google.com: an advertiser page or a creative page (that advertiser is searched) and a domain search page (that domain is searched). A `region=PL` in the links sets `region` when it is empty. Other links are saved as free rows with an `error`. |
| `domains` | Domains the ads point to, one search each. In domain check mode, the domains to check (up to 1,000). Full URLs work: the actor keeps the host and drops `www.`. |
| `domainsText` | The same domains as one text, separated by line breaks, spaces, commas or semicolons. |
| `region` | Country as a two-letter code (`US`, `PL`, `DE`, `UK`) or an English name (`Poland`, `United Kingdom`). Empty means all countries. |
| `dateFrom`, `dateTo` | Ads shown at some point inside this range (`YYYY-MM-DD`). Google allows this filter only for the last 364 days. If you set one date, the actor fills the other. |
| `shownInLastDays` | Ads shown at some point in the last N days, today included (1 to 364). An ad that started months ago and still runs is included; for only new ads use `onlyNewSinceLastRun`. Use it instead of fixed dates in saved tasks and schedules, so the range moves with every run. |
| `format` | `ALL`, `TEXT`, `IMAGE` or `VIDEO`. |
| `platform` | `ALL`, `SEARCH`, `YOUTUBE`, `SHOPPING`, `MAPS` or `PLAY`: only ads Google showed on that platform. |
| `videoLinks` | Adds `videoUrl` and `thumbnailUrl` to video ads. On by default for the Video format, off for the others. |
| `previewImages` | On by default. For image ads that Google only shows through a preview, reads that preview file (about 0.2 MB, 4 at a time) and fills `imageUrl`. It adds roughly 0.25 s per such ad to the run and does not change the price. Turn it off for the fastest run. |
| `maxAds` | Ads per search. Maximum 100,000. When you do not send it, each search stops at 100 ads and the log opens with a warning that names the cap and the most a run can charge. |
| `adDetails` | Off by default. Reads each ad's detail page and adds `impressionsMin`, `impressionsMax`, `impressionsAvailableFrom`, `targeting` and `regionStats`. This is one extra request per ad, so runs take longer. The price per ad stays the same. |
| `downloadCreatives` | Off by default. Saves each ad's image as a file in the run's key-value store and adds `creativeFileUrl`. Stored files are free for now (a per-file price will be announced on the Pricing tab with the usual two weeks' notice). See [Download the ad images](#download-the-ad-images). |
| `onlyNewSinceLastRun` | Off by default. Saves only ads this search has not returned in earlier runs. See [Monitor new ads](#monitor-new-ads). |
| `monitorName` | Optional name of the list of seen ads, for `onlyNewSinceLastRun`. Empty means one list per search. The old name `stateKey` still works. |
| `includeLastShownChanges` | With `onlyNewSinceLastRun`, also saves ads seen before whose last-shown date moved. |
| `onlyWithProducts` | Keep only shopping ads that come with product data. The other ads are skipped before saving, so you are not charged for them, and `maxAds` counts the ads kept. |
| `proxyConfiguration` | Apify Proxy is on by default. When Google answers with a CAPTCHA page, the actor retries on a new IP. With the default Apify Proxy, the first CAPTCHA moves the run to Apify residential proxy when the run has access to it. |

### Output

A real video ad row:

```json
{
    "searchQuery": "Nike, Inc.",
    "resolvedBy": "advertiser",
    "matchedAdvertiser": "Nike, Inc.",
    "advertiserId": "AR16735076323512287233",
    "advertiserName": "Nike, Inc.",
    "domain": null,
    "creativeId": "CR07320027051652546561",
    "format": "VIDEO",
    "firstShown": "2026-07-07T14:21:15.000Z",
    "lastShown": "2026-10-03T03:04:46.000Z",
    "daysShown": 89,
    "imageUrl": null,
    "videoUrl": "https://www.youtube.com/watch?v=grj7FyOMWh8",
    "thumbnailUrl": "https://i.ytimg.com/vi/grj7FyOMWh8/hqdefault.jpg",
    "landingUrl": "https://www.nike.com/ca/retail",
    "previewScriptUrl": "https://displayads-formats.googleusercontent.com/ads/preview/content.js?client=ads-integrity-transparency&...",
    "transparencyUrl": "https://adstransparency.google.com/advertiser/AR16735076323512287233/creative/CR07320027051652546561",
    "headline": null,
    "description": null,
    "displayUrl": null,
    "businessName": null,
    "sitelinks": [],
    "products": []
}
```

| Field | Description |
|---|---|
| `searchQuery` | The name, domain or ID list that found the ad. |
| `resolvedBy` | How the search found its target: `advertiser` (a name matched an advertiser), `domain` (a name was searched as the brand's domain), `id` or `domain-input` (you gave IDs or a domain). |
| `matchedAdvertiser` | The advertiser name, domain or ID list the search ran on. |
| `advertiserId`, `advertiserName` | Advertiser as Google shows it. |
| `domain` | Filled when the search ran on a domain. |
| `creativeId`, `transparencyUrl` | The ad's ID and its page in the Transparency Center. |
| `format` | `TEXT`, `IMAGE`, `VIDEO`, or `UNKNOWN` for a format code the actor does not know. |
| `firstShown`, `lastShown`, `daysShown` | First and last time Google served the ad, in UTC, and the number of days Google served the ad, as the Transparency Center reports it (when Google leaves it out, the calendar days from the first to the last, both included). |
| `imageUrl` | An image of the ad, when Google has one. Most text ads have it with Google's own image link. Image ads without one get Google's thumbnail of the ad image or product photo (`encrypted-tbn…gstatic.com`) from their preview, unless `previewImages` is off or the preview could not be read. Text ads that carry only a preview, and video ads, keep it empty. |
| `creativeFileUrl` | With `downloadCreatives` on, the link to the ad's image file in the run's key-value store. Null when `downloadCreatives` is off, the ad has no image or the download failed. |
| `videoUrl`, `thumbnailUrl` | YouTube link and thumbnail of a video ad. |
| `landingUrl` | The landing page URL of the ad, when Google's preview includes it. Filled for some video and image ads only (from the preview file the actor already reads for `videoUrl` and `imageUrl`); null for text ads and for every ad whose preview has no advertiser link. No extra requests and no extra cost. |
| `previewScriptUrl` | Google's preview script for ads that Google renders in a browser. It is a JavaScript file that draws the ad in a browser, not an image. |
| `products` | Shopping ads: `{ title, price, currency, seller, url }` for each product. |
| `headline`, `description`, `displayUrl`, `businessName`, `sitelinks` | Ad text, only when Google puts it into the preview. |

#### EU ad details

With `adDetails` on, every row gets these fields. A real Allegro search ad shown in Poland and Germany:

```json
{
    "impressionsMin": 20000,
    "impressionsMax": 25000,
    "impressionsAvailableFrom": null,
    "targeting": {
        "included": ["demographics", "locations", "contextualSignals"],
        "excluded": ["contextualSignals", "customerLists"]
    },
    "regionStats": [
        {
            "country": "PL",
            "firstShown": "2023-03-01",
            "lastShown": "2026-10-03",
            "impressionsMin": 20000,
            "impressionsMax": 25000,
            "platforms": [
                { "platform": "YOUTUBE", "impressionsMin": 0, "impressionsMax": 1000 },
                { "platform": "GOOGLE_SEARCH", "impressionsMin": 20000, "impressionsMax": 25000 }
            ]
        },
        {
            "country": "DE",
            "firstShown": "2023-03-04",
            "lastShown": "2026-08-23",
            "impressionsMin": 0,
            "impressionsMax": 1000,
            "platforms": [{ "platform": "GOOGLE_SEARCH", "impressionsMin": 0, "impressionsMax": 1000 }]
        }
    ]
}
```

| Field | Description |
|---|---|
| `impressionsMin`, `impressionsMax` | Google's impression range for the ad, summed over the countries where Google reports impressions. The top range is open, so an ad shown more than 10 million times has `impressionsMin` 10000000 and `impressionsMax` null. |
| `impressionsAvailableFrom` | Google publishes impressions about 90 days after an ad starts. Until then the range is null and this field holds the date Google gives for it. |
| `targeting` | Audience targeting categories from the advertiser's latest campaign: `demographics`, `locations`, `contextualSignals`, `topicsOfInterest` and `customerLists`. A category can be in both lists when some of its criteria were included and others excluded. Categories the advertiser did not use are in neither list. Google shows the categories only, not the exact ages, places or topics. |
| `regionStats` | One entry per country where the ad ran, the countries with the most impressions first: `country` (two-letter code), `firstShown` and `lastShown` dates, the impression range in that country and its split by `platforms` (`GOOGLE_SEARCH`, `YOUTUBE`, `GOOGLE_SHOPPING`, `GOOGLE_MAPS`, `GOOGLE_PLAY`). |

These fields are null when `adDetails` is off. If the details of an ad cannot be read, the ad is still saved without them and its `regionStats` is null, while an ad whose details were read always has a `regionStats` list. The log says how many ads on each page were affected, and the run's status message gives the total. After 20 failed detail requests in a row the actor stops asking for details for the rest of the run and says so in the status message.

What we measured on 2026-10-04:

- Google gives impressions and targeting for ads shown in the EU. We also saw impression ranges for Norway, Iceland and Turkey. For other countries, such as the US, Brazil or Japan, `regionStats` has only the last shown date and the ad has no impressions or targeting.
- On the newest 37 ads of Allegro, Zalando, mBank, InPost and Lidl, all 37 had targeting and 19 had an impression range. The other 18 had started less than 90 days before, and each of them had an `impressionsAvailableFrom` date.
- A US Nike text ad and a US Nike video ad came back with one country entry, the US, with its last shown date only.

What to expect from the other fields, from our test runs:

- Dates, advertiser, format and the ad page link are filled for every ad.
- 21 of 30 Samsung video ads and 26 of 40 video ads leading to nike.com came with a YouTube link. The other video ads had an image instead, or their preview held no YouTube video.
- Product data came with 12 of 40 Allegro image ads in Poland. Brands that do not run shopping ads have none.
- Ad text is rare: Google put a headline into the preview for about 1 to 2 ads in 100.

### Download the ad images

Google's image links can stop working when an ad is taken down. Turn on `downloadCreatives` to keep a copy: the actor downloads the image in `imageUrl` of each saved ad (the archived ad image or the picture from its preview) and stores it in the run's default key-value store under `creative-<creativeId>.<ext>`, for example `creative-CR13820127804068462593.png`. The row gets `creativeFileUrl`, the link to that record:

```json
{
    "creativeId": "CR13820127804068462593",
    "format": "IMAGE",
    "imageUrl": "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9Gc...",
    "creativeFileUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/creative-CR13820127804068462593.png"
}
```

- Video ads are not downloaded as video. They keep their YouTube link in `videoUrl`; only when Google has an archived still image of a video ad in `imageUrl` is that image stored.
- Only JPEG, PNG, GIF, WebP and AVIF files up to 5 MB are stored. Each download gets 20 seconds, 4 run at a time.
- A failed download leaves `creativeFileUrl` null, is not charged and never fails the run. After 5 failed downloads in a row the actor stops downloading for the rest of the run, saves the remaining ads with `creativeFileUrl` null and says so in the status message.
- With `previewImages` off, only images that Google archives are stored, not the pictures from ad previews.
- Stored files are free for now; a per-file price will come later with two weeks' notice. Ads without an image cost the same as before.

### Ready-made searches

Open one of these, change the input and run it:

- [See every Google ad a competitor is running](https://apify.com/solidfetch/google-ads-transparency-scraper/examples/competitor-google-ads)
- [Find who advertises on Google for a domain](https://apify.com/solidfetch/google-ads-transparency-scraper/examples/domain-advertisers)
- [Track Google Shopping ad prices for a retailer](https://apify.com/solidfetch/google-ads-transparency-scraper/examples/google-shopping-ad-prices)
- [Download a brand's Google and YouTube video ads](https://apify.com/solidfetch/google-ads-transparency-scraper/examples/google-video-ads)
- [See which Google ads a brand runs in one country](https://apify.com/solidfetch/google-ads-transparency-scraper/examples/google-ads-by-country)
- [Get all ads of a Google advertiser ID](https://apify.com/solidfetch/google-ads-transparency-scraper/examples/google-ads-by-advertiser-id)
- [Find a brand's Google ads from the last 30 days](https://apify.com/solidfetch/google-ads-transparency-scraper/examples/google-ads-date-range)

### Use it from Clay, Google Sheets or Make

These tools call the actor through the Apify API and send a spreadsheet column as one value, so the text fields take a whole column. This call asks "does this domain run Google ads?" for a list of domains and returns one row per domain, with your own value in `input`:

```bash
curl -X POST "https://api.apify.com/v2/acts/solidfetch~google-ads-transparency-scraper/run-sync-get-dataset-items?token=<YOUR_TOKEN>&maxTotalChargeUsd=1" \
  -H "Content-Type: application/json" \
  -d '{"mode": "domainCheck", "domainsText": "nike.com\nzalando.pl\nallegro.pl", "region": "PL"}'
```

- **`domainsText` and `advertiserNamesText`** take one text: one value per line, or separated by commas, semicolons or spaces for domains. A brand name with a comma (`Nike, Inc.`) goes on its own line. The same fields work in `ads` and `advertisers` mode. The `domains` and `advertiserNames` lists keep working, and also take a single string through the API.
- **Map rows back with `input`.** Every domain check row carries the value you sent. Values that are not domains (an IP address, an e-mail address, an empty cell) come back as free rows with `hasAds: null` and an `error`, so the row count matches your list, apart from repeated domains, which are checked once.
- **Set the limits in the caller.** In `ads` mode send `maxAds` (ads per search) and the maximum cost per run: add `maxTotalChargeUsd=<dollars>` to the API URL, or set it in the integration. A tool that does not pass `maxAds` gets the default of 100 ads per search, and the run log starts with a warning that says so. Domain check mode charges one answer per domain and needs only the maximum cost.
- **Pasted Transparency Center links** go in `startUrls` (`{"startUrls": [{"url": "https://adstransparency.google.com/advertiser/AR16735076323512287233"}]}`) in `ads` mode.
- Google Sheets and Make read the rows from the finished run's dataset, see [Send results to Make, Zapier, n8n or a CRM](#send-results-to-make-zapier-n8n-or-a-crm).

### Send results to Make, Zapier, n8n or a CRM

Long runs do not need to keep a Make or Zapier scenario waiting:

1. Start the actor or one of its saved tasks on a schedule, or from your scenario with the asynchronous "Run Actor" step.
2. Trigger the next step on the finished run. In Make use the Apify module "Watch Actor Runs", in Zapier the Apify trigger for a finished actor run, in n8n the Apify node.
3. Read the rows with "Get Dataset Items" using the run's dataset ID.

For your own endpoint, open the actor's Integrations tab and add an HTTP webhook on "Run succeeded". Apify retries failed deliveries, and the payload contains `resource.defaultDatasetId`, which you can read at `https://api.apify.com/v2/datasets/<datasetId>/items?format=json`. Add `&format=csv` for a spreadsheet, or `&flatten=<field>` to turn nested objects into flat columns.

### Pricing

You pay $1.50 per 1,000 saved ads on the free plan, $1.20 on Bronze, $1.00 on Silver and $0.80 on Gold and above. Every run also pays a start fee of $0.00005, including runs that fail or save nothing. A run that saves no ads costs only that start fee. If a run fails partway, the ads it saved before the failure stay in the dataset and stay charged. There is no extra charge for platform usage, and the run stops when it reaches the maximum cost per run that you set. The $5 of monthly free credit covers about 3,300 ads.

With `downloadCreatives` on, stored image files are free for now. A per-file price will be added later, announced on the Pricing tab two weeks ahead. Failed downloads and ads without an image will never be charged for a file.

Domain check mode is charged per checked domain instead of per ad: $6 per 1,000 domains on the free plan, less on paid plans, from 28 October 2026. Until then domain checks are free; you pay only the small start fee. Domains that could not be checked are never charged.

Advertiser lookup mode is free for now; a per-advertiser price will be added later with two weeks' notice. Names and domains without advertisers, and lookups that failed, will never be charged.

### FAQ

**Why do I get fewer ads than the Transparency Center website shows?** The website shows a rounded estimate ("about 9,000 ads"). The actor returns the ads Google actually lists, and `maxAds` limits each search.

**Can I get ads older than a year?** Yes, leave the dates and `shownInLastDays` empty. Google allows the date filter only for the last 364 days.

**Why are impressions empty for some EU ads?** Google publishes the impression range about 90 days after an ad starts. `impressionsAvailableFrom` shows the date. Ads that ran only outside the EU have no impressions or targeting at all.

**Does it need a Google account or cookies?** No. It reads the same public data as the website.

**Can I run it on a schedule?** Yes. Save your input as a task in Apify with `shownInLastDays` instead of fixed dates and add a schedule, for example every Monday. Turn on `onlyNewSinceLastRun` to get only the ads that are new since the last run.

### Use it from code or AI agents

```bash
curl -X POST "https://api.apify.com/v2/acts/solidfetch~google-ads-transparency-scraper/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"advertiserNames": ["Nike"], "maxAds": 50}'
```

The actor is pay per event with limited permissions, so it works through the Apify MCP server in Claude, Cursor and other AI agents, and with Apify integrations for Make, n8n, Zapier and Google Sheets.

### Notes

- A name, domain or ID list that finds no ad is named in the run's status message (`Saved 4 ads. No ads for: shop.nike.com`), and a name that matches no advertiser is listed too. An input value that cannot be searched (a bad advertiser ID, a link that is not a Transparency Center page) is saved as a free row with `searchQuery` and `error`.
- The country filter uses Google's own targeting data.
- If Google changes its response format, the run fails with an error instead of finishing with an empty dataset.
- Found a problem? Open an issue on the actor page and include the input you used.
- When a run fails, an error report with the error message and the shape of the input (which fields are set, how long the lists are), never your input values or your results, is sent to the developer to fix bugs.

This is an independent tool and is not affiliated with Google.

### Related actor

[Core Web Vitals Competitor Benchmark](https://apify.com/solidfetch/crux-competitor-benchmark) compares the real-user page speed (LCP, INP, CLS) of the same competitors, from Google's Chrome UX Report.

If this actor saved you time, a short review on its Store page helps other people find it. If something broke or a field is missing, open an issue and I will answer within a day.

# Actor input Schema

## `mode` (type: `string`):

Ads: every ad of the brands, domains or advertiser IDs below, charged per ad. Domain check: one row per domain in "Domains" saying whether it advertises on Google, with up to 5 advertisers, the rounded ad count, the most recent shown date and the formats seen, charged per domain. Domain check uses only Domains and Country. Advertiser lookup: one row per advertiser account that matches a name in "Brand or advertiser names" or runs ads for a domain in "Domains", with its ID, country, rounded ad count and the account the Ads mode would pick, charged per advertiser. Advertiser lookup uses only names, Domains and Country.

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

One search per name, for example Nike, Allegro, Temu. The actor picks the matching advertiser with the most ads, or the brand's domain when the ads run under another legal name. Names that differ only in case or punctuation are searched once. Advertiser lookup: every advertiser whose name contains the name, up to 500 names and domains per run. To paste a column from a spreadsheet, use "Names as text" below.

## `advertiserNamesText` (type: `string`):

The same as "Brand or advertiser names", pasted as one text: one name per line, or names separated by commas when there is no line break. A name with a comma goes on its own line. Meant for tools such as Clay and Google Sheets that send a whole column as one value.

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

Ads: one search per domain, ads from every advertiser that sends traffic there, for example nike.com. Domain check: the list of domains to check, up to 1,000 per run, one row each with your value in "input". Advertiser lookup: the advertisers whose ads lead to the domain. Full URLs work too; the scheme, path and www. are removed, internationalised names are saved as punycode and duplicates are checked once. IP addresses, e-mail addresses and names without a public suffix are not domains: domain check returns a free row with an error for them. To paste a column from a spreadsheet, use "Domains as text" below.

## `domainsText` (type: `string`):

The same as "Domains", pasted as one text: separated by line breaks, spaces, commas or semicolons. Meant for tools such as Clay and Google Sheets that send a whole column as one value.

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

Advertiser IDs (AR followed by 20 digits) or advertiser page URLs from the Transparency Center. All IDs are searched together. An ID that is not valid is skipped and saved as a free row with the reason; the valid ones are still searched.

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

Links copied from adstransparency.google.com: an advertiser page or a creative page (that advertiser is searched, joined with the IDs above) and a domain search page (that domain is searched). A link with a country (region=PL) sets Country when it is empty. Anything else is skipped and saved as a free row.

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

Country code (ISO 3166-1, e.g. US, PL, DE) or English country name (e.g. Poland, United Kingdom). Leave empty for all countries. Domain check: whether the domain has ads shown in this country. Advertiser lookup: the best match prefers an advertiser from this country, and a domain lists the advertisers of ads shown here.

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

Only ads shown at some point on or after this date (YYYY-MM-DD). An ad that started earlier and still ran after it is included. For ads that are new since a date, use onlyNewSinceLastRun.

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

Only ads shown at some point on or before this date (YYYY-MM-DD).

## `shownInLastDays` (type: `integer`):

Only ads shown at some point in the last N days, today included (1 to 364). This is not the same as ads that started in that window; for new ads use onlyNewSinceLastRun. Use it instead of the two dates in saved tasks and schedules, so the range moves with every run.

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

Filter by ad format.

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

Only ads Google showed on this platform: Google Search, YouTube, Google Shopping, Google Maps or Google Play.

## `videoLinks` (type: `boolean`):

Adds videoUrl and thumbnailUrl to video ads by reading one preview file per video ad. When left empty, it is on for the Video format and off for the others.

## `previewImages` (type: `boolean`):

Fills imageUrl for image ads that Google only shows through a preview, by reading one preview file per such ad (about 0.2 MB each, 4 at a time). Turn it off for the fastest run. When left empty, it is on.

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

Each name, the ID list and each domain stop after this many ads. Ads found by an earlier search are not saved or charged again. When left empty, each search stops at 100 ads and the log says so. Set it when you start the actor from Clay, Make or another tool, together with the maximum cost per run.

## `onlyWithProducts` (type: `boolean`):

Keep only shopping ads that come with product titles and prices. Other ads are skipped before saving, so they are not charged. Max ads then counts the ads kept.

## `adDetails` (type: `boolean`):

Adds impressionsMin, impressionsMax, targeting and regionStats (per-country first and last shown dates and impression ranges) by reading each ad's detail page. Google publishes impressions and targeting for ads shown in the EU and a few other European countries. Costs one extra request per ad, so runs take longer.

## `downloadCreatives` (type: `boolean`):

Saves the image of each ad (the archived ad image or the picture from its preview) as a file in the run's key-value store and adds its link as creativeFileUrl. Video ads keep their YouTube link, the video itself is not downloaded. Each stored file is charged as one creative-file event on top of the ad. Files over 5 MB and failed downloads are skipped and not charged.

## `onlyNewSinceLastRun` (type: `boolean`):

For scheduled runs: save only ads this search has not returned before. The first run saves every matching ad as the baseline. Ads seen by earlier runs are skipped and not charged. Google lists the most recently shown ads first, so Max ads per search is how deep each run looks.

## `monitorName` (type: `string`):

Optional. Runs with the same name share the list of ads already seen. Leave empty to get one list per search: the names, domains, IDs, country and format. Letters, digits and !-\_.'() only.

## `includeLastShownChanges` (type: `boolean`):

Also save ads seen before whose last-shown date is later than in the last run, that is ads still running. These rows have changeType lastShownChanged and previousLastShown.

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

Google answers with a CAPTCHA page when one IP sends many requests. The actor then switches to a new proxy IP and retries. With the default Apify Proxy, the first CAPTCHA moves the run to Apify residential proxy when the run has access to it. Apify Proxy is recommended.

## Actor input object example

```json
{
  "mode": "ads",
  "advertiserNames": [
    "Nike, Inc."
  ],
  "format": "ALL",
  "platform": "ALL",
  "previewImages": true,
  "maxAds": 50,
  "onlyWithProducts": false,
  "adDetails": false,
  "downloadCreatives": false,
  "onlyNewSinceLastRun": false,
  "includeLastShownChanges": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

## `videoAds` (type: `string`):

No description

## `adText` (type: `string`):

No description

## `shoppingAds` (type: `string`):

No description

## `euAdDetails` (type: `string`):

No description

## `domainCheck` (type: `string`):

No description

## `advertisers` (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 = {
    "advertiserNames": [
        "Nike, Inc."
    ],
    "maxAds": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("solidfetch/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": ["Nike, Inc."],
    "maxAds": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("solidfetch/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": [
    "Nike, Inc."
  ],
  "maxAds": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call solidfetch/google-ads-transparency-scraper --silent --output-dataset

```

## MCP server setup

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