# Google Ads Transparency Report: Competitor Ads (`firsthand/google-ads-transparency-report`) Actor

Google Ads Transparency Center scraper for competitor monitoring: every ad by domain, advertiser or brand, with run dates, countries, formats and ad copy read from the ad or its image (OCR). Get a weekly report of new, stopped and long-running ads. Never reports zero ads when Google refused.

- **URL**: https://apify.com/firsthand/google-ads-transparency-report.md
- **Developed by:** [Volodymyr Cherniaga](https://apify.com/firsthand) (community)
- **Categories:** SEO tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 ads

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

## Google Ads Transparency Report: Competitor Ads

### What is Google Ads Transparency Report?

Google Ads Transparency Report is a competitor ad monitoring tool: it scrapes the
[Google Ads Transparency Center](https://adstransparency.google.com/) and turns what it finds into
data you can use. For any domain, advertiser id or brand name you get every Google ad they are
running, a competitor report of new, stopped and long-running ads for any period, and the ad copy
itself, grouped into unique messages.

The Transparency Center shows one advertiser at a time, on screen, with no history and no export.
This Actor reads many competitors at once, on a schedule, and hands you tables you can export to
CSV, Excel or JSON, send to Google Sheets, or pull through the Apify API.

Use it for Google Ads competitor research, ad spy and ad intelligence work, PPC creative
benchmarking, landing page and UTM analysis, or to watch a market's Search, YouTube, Shopping,
Maps and Play ads week by week.

**Tell us what you need.** This Actor is actively developed and we turn requests around fast. If a
field is missing, a filter is not there, or you need a different output shape, open an issue on the
Actor page or leave a review with what you need. Concrete requests get built.

### What you get

| Table | One row per | What is in it |
|---|---|---|
| Ads (default dataset) | ad creative | domain, advertiser, format, first and last shown date, days shown, image or preview link, Transparency Center link; with details: countries, impression ranges, headline, description, visible URL, landing URL and its UTM tags; sitelinks come from OCR only |
| targets | domain, advertiser or brand you asked for | status `ok`, `empty`, `partial`, `failed` or `skipped` and the reason |
| report | competitor and advertiser | ads running now, new, stopped and long-running in the period, by format and country, with examples |
| messages | unique ad message | headline and description, how many creatives use it, first and last shown, formats, visible URL, UTM campaigns where the ads carry them |

The ads are on the run's **Output** tab. The run's log links the other three tables, and so does
the **OUTPUT** record on the **Storage** tab.

**"No ads" is never a guess.** If Google refused a request, the target is marked `failed` with the
reason, not `empty`. A list cut by your limit is `partial`, and a target the run did not get to is
`skipped`.

**An empty cell always says why.** Every ad row has a **Details** and an **OCR** column: `off` for a
switch you left off, `skipped` with what to change to get the text, `failed` when Google refused.
An empty headline never silently means "this ad has no text".

#### Landing URL, UTM tags and visible URL: what Google actually publishes

How much of an ad's link you get depends on the format, because Google archives formats
differently. Measured on 22 September 2026 on 250 ads across 50 company domains:

| Ad format | `destination_url` (landing URL) | `utm_source` and other UTM tags | `display_url` (visible URL) |
|---|---|---|---|
| Text | 0% | 0% | 93% |
| Image | 14% | 8% | 4% |
| Video | 94% | 36% | 97% |

- **Text ads carry no link at all.** Google archives them as a picture, with no URL anywhere, so no
  tool can extract one. The Actor reads the **visible URL** off the picture with OCR instead
  (`www.hubspot.com/free/crm`), for 93% of text ads.
- **Video ads are where UTM analysis works.** Set *Ad format* to `video` if UTM data is what you
  are after. Whether UTM tags are there is the advertiser's own choice.
- Every field is either filled from the source or left empty; nothing is guessed.

### How to use it

1. **One-off look at a competitor.** Add domains (`hubspot.com`), advertiser ids, Transparency
   Center links or brand names. Run.
2. **Weekly competitor report.** Set *Competitor report period* to 7 and schedule the Actor weekly.
   It reads only the recent part of each list, so a weekly run stays cheap even on a large
   advertiser.
3. **Ad copy and messaging matrix.** Turn on *Fetch ad text, countries and impressions* and
   *Read text from ad pictures (OCR)*. The *messages* table then lists every unique message
   each competitor runs.

### Who uses Google Ads competitor monitoring?

- **PPC and performance marketing teams** checking which creatives a competitor keeps running for
  months, and which ones they killed in a week.
- **SEO and content teams** pulling competitor ad copy to see the exact wording competitors pay for.
- **Agencies** reporting on a client's competitive set every week without opening the Transparency
  Center by hand.
- **Market and product researchers** mapping who advertises in a category, in which countries, and
  on which Google surfaces.
- **Brand protection teams** spotting third parties, resellers and affiliates that advertise on
  their brand's domain. In a 50-domain sample the Actor found a median of 4 advertisers per domain
  and up to 40 on one.

### How much does it cost to monitor competitor ads?

You pay only for what the run returns (pay per event). The competitor report and the messages
table are free: they are built from the ads you already paid for.

| Event | Free | Bronze | Gold | Charged when |
|---|---|---|---|---|
| Actor start | $0.0035 | $0.0030 | $0.0023 | once per run, after your input has been accepted |
| Ad | $0.0005 | $0.0004 | $0.0003 | an ad row is returned |
| Ad details | $0.0002 | $0.00016 | $0.00012 | countries and impression ranges are read for an ad |
| Ad text | $0.0026 | $0.0020 | $0.0015 | headline, description and landing URL are read from the source |
| Ad text by OCR | $0.0013 | $0.0010 | $0.0008 | the text is read from the picture of a text ad |

Silver sits between Bronze and Gold; Platinum and Diamond pay the Gold price.

Example: a weekly report on 10 competitors with about 100 ads each in the week, with details and
ad copy for the first 20 ads of each competitor. On the free plan that is **$0.78 a week**, on
Bronze **$0.62**.

**What you pay for here is the ad copy.** The ad list costs $0.50 per 1,000 ads, a third of what
the two most used Google Ads Transparency Actors charge for it. The Actor also reads the text off
the picture of a text ad, which is how Google stores most of them, where most other Actors return
only the picture. Ad copy for every ad brings the price to about $0.0019 per ad on the free plan,
so use *Max ads with details per target* to decide how much of it you buy.

To keep a run within budget, use *Max ads per domain or advertiser*, *Max ads with details per
target*, or the run's **Max total charge**. At the limit the Actor stops and marks the targets it
did not read, instead of returning a half-empty list that looks complete.

### Input

- **Competitor domains**, **Advertiser ids**, **Transparency Center links**, **Brand names**:
  what to look up. **Advertisers per brand name** caps how many advertisers one name expands into.
- **Country**, **Ad format**, **Platform** (Search, YouTube, Shopping, Maps, Play),
  **Shown on or after / before**: filters.
- **Competitor report period** and **Long-running ad after**: the report.
- **Max ads per domain or advertiser**, **Max ads with details per target**: cost caps.
- **Ads fetched at the same time**: speed, 6 by default.
- **Proxy**: Apify's Residential proxy, set by default.

### Limitations

These come from what Google publishes in the Transparency Center:

- **Most text ads are archived only as a picture.** With OCR on, the Actor reads the text from it;
  such rows are marked `text_source: "ocr"` with a confidence score. Non-Latin scripts are left
  empty rather than filled with garbage.
- **Text ads have no landing URL**, see the table above.
- **No keyword search across all advertisers.** The Transparency Center searches by advertiser or
  domain only.
- **One country per run**, as in the Transparency Center itself.
- **Platform filter** covers ads shown on or after 4 September 2023.
- **Impression ranges** are published only for some ads, mostly those shown in the EU.
- The Transparency Center has no public API. If Google changes it, the Actor can break until it
  is updated.

### Missing a field or a filter? Ask, and it gets built

Need another field, a filter or a different output shape? Open an issue on the Actor's **Issues**
tab or leave a review saying what you need and what you do with it. Something wrong in the data?
Send the run id and the row: you get a fix, or a straight answer if the source does not publish it.

### FAQ

**Why use this instead of the Transparency Center itself?** It shows one advertiser at a time and
no history you can work with. This Actor reads a list of competitors on a schedule, tells you what
is new, what stopped and what has been running for months, extracts the ad copy, and gives you
tables instead of a page you have to scroll.

**Can I get the landing page URL and UTM parameters of a competitor's ads?** For video ads, mostly
yes; for image ads, about one in seven; for text ads Google publishes no link, and you get the
visible URL read from the ad picture instead. See the table above.

**How many ads does it return per competitor?** Whatever the Transparency Center holds, up to your
own cap. One large SaaS domain returned 3,531 ads from 528 advertisers in one run. A run reads up
to 10,000 ads per target and 40,000 in total; a longer list is marked `partial`.

**What is the difference between `empty`, `failed` and `skipped`?** `empty`: the Transparency Center
answered and has no ads for this target. `failed`: it did not answer after retries, so nothing is
known. `skipped`: the run did not ask, because it reached its cost, time or row limit first.

**How does the scheduled report count?** The period is exactly N days back from the start of the
run. An ad is running now if it was shown in the last 48 hours, and stopped if it crossed that mark
during the period. To see those stops, the Actor also reads the ads shown in the 48 hours before the
period (up to 72, rounded to a UTC day); those rows are in the output and charged like any other.

**Why is a proxy required?** Google blocks a single IP address after a few dozen requests. Only
Apify's Residential proxy gets through; any other Apify proxy group is switched to it, and the proxy
costs you nothing either way.

**How long does a run take?** The list is fast. Ad text takes a few seconds per ad, 6 ads at a time,
so about 1,000 to 1,500 ads with text fit into the default 1-hour timeout. Near the timeout the
Actor stops on its own: what it read stays in the dataset, the rest is marked `skipped`, and you pay
only for what was read. The same holds if you abort a run.

**Is it legal?** The Actor reads data Google publishes publicly in the Ads Transparency Center,
without logging in. This Actor is not affiliated with Google. Check the rules that apply to your
use of the data.

# Actor input Schema

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

Domains to look up in the Google Ads Transparency Center (hubspot.com). Links with https://, www. and a path are fine. A domain returns ads from every account advertising it, agencies and job boards included, so each row carries both the domain and the advertiser.

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

Advertiser ids from the Transparency Center, the AR... part of a link like adstransparency.google.com/advertiser/AR04545223091738902529. Rows found this way have no domain: the source does not return one for advertiser lookups.

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

Links copied from adstransparency.google.com: an advertiser page (/advertiser/AR...) or a domain search (?domain=...). A link to a single ad returns its advertiser. The region in a link is used when the Country field below is empty.

## `searchQueries` (type: `array`):

Company or brand names (Zalando, Domino's). The Transparency Center's own name search finds advertisers whose name matches, and the actor returns their ads. It matches advertiser names, not the text of ads. Each ad found by a name says which name found it, unless another input of the same run returned that ad first.

## `maxAdvertisersPerQuery` (type: `integer`):

How many of the matching advertisers to take for each brand name, in the order the Transparency Center lists them. Big brands have several accounts per country.

## `country` (type: `string`):

Two-letter country code (US, DE, JP). Only ads shown in this country. Leave empty for all countries. One country per run: given several, the source uses only the first.

## `adFormat` (type: `string`):

Only text, image or video ads.

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

Only ads shown on one Google platform. The Transparency Center's platform filter only covers ads shown on or after 4 September 2023; older ads never appear when a platform is chosen.

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

Only ads shown on or after this day (YYYY-MM-DD). Every ad is checked against its own first and last shown dates. The Transparency Center lists ads from the most recently shown, so reading stops at the first ad last shown before this day; if the run sees that order broken, it reads the whole list instead. The targets report says how many ads were left out.

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

Only ads first shown on or before this day (YYYY-MM-DD).

## `fastPeriodFilter` (type: `boolean`):

Pass the period to the Transparency Center's own date filter: about half the requests and time on large advertisers, but that filter misses some ads, mostly long-running ones: 0.7% for the last 30 days and up to 5% for older months (measured 11.09.2026). It only reaches about 11 months back; older periods are filtered the complete way anyway.

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

Safety cap, counted after filters. Large brands run thousands of ads. A target cut by this cap is reported with status partial in the targets report, never as complete.

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

Adds the countries where each ad was shown, impression ranges where the Transparency Center publishes them (mostly for ads shown in the EU) and, where it keeps the ad as HTML, its text: headline, description, display and landing URL (with the campaign's UTM tags when the ad has them) and the YouTube video id for video ads. Many text ads are archived only as a picture of the ad: then image\_url holds that picture, text\_available is false and the text fields stay empty. How much of the link you get depends on the format, measured 22.09.2026 on 250 ads across 50 domains: the landing URL and its UTM tags come with 94% of video ads (UTM tags in 36% of them, the advertiser's own choice), about 14% of image ads and 0% of text ads, because Google publishes no link for a text ad at all; for those the display URL is read off the picture instead, in 93% of cases, when 'Read text from ad pictures (OCR)' is on. The countries and impressions card is one small request per ad and is charged as the ad details event. Ad text read from the source is charged as a separate, pricier ad text event, because the ad preview file it comes from is about 300 KB.

## `ocrTextAds` (type: `boolean`):

Most text ads (94% of 6,095 on 194 company domains, measured 11.09.2026) are archived by the Transparency Center only as a picture of the ad, with no text. This reads the headline, description, display URL and sitelinks from that picture with Tesseract OCR. It runs on text ads only, because those are the ones the source archives as a picture; image and video ads say so in their OCR column. Recognised text can contain OCR errors, so such rows have text\_source "ocr" and an ocr\_confidence score; text taken from the source itself has text\_source "source". Some Maps ads are archived as an empty layout with no text at all; they are marked in ocr\_status. Charged as a separate event, only for ads whose text was read.

## `maxDetailsPerTarget` (type: `integer`):

Fetch text, countries and impressions (and OCR text) only for the first N ads of each target, in the order the Transparency Center lists them. The other ads still come back with their format, dates and preview, but their text fields stay empty and their Details and OCR columns say they were past this limit. Use it to cap the cost of text; leave it empty to get text for every ad.

## `enrichWorkers` (type: `integer`):

How many ads have their details and OCR read in parallel. Measured 22 September 2026: about 10 seconds per enriched ad one at a time and 2 to 3.5 seconds six at a time, and the run waits on the network for almost all of it, so raising this is what makes a large run fit into the run timeout. Lower it to 1 if the Transparency Center starts refusing your requests: the run gets slower but finishes.

## `reportDays` (type: `integer`):

Adds a competitor report for the last N days: one row per domain and advertiser with ads running now, new ads, stopped ads and long-running ads, by format and by country, plus the unique ad messages (headline and description) with how many creatives use each. An ad counts as running now when it was last shown within 48 hours: the Transparency Center updates this date with a delay of up to a day and a half (measured on 10,888 ads). An ad counts as stopped when it passed that 48-hour mark during the period, so daily reports on a schedule never miss a stop. When Shown on or after is empty, reading starts two days before the period (to see those stops) and stops at the first older ad, so scheduled runs stay cheap. Messages need ad text: turn on details or OCR.

## `longRunnerDays` (type: `integer`):

An ad running now that has been shown for at least this many days counts as long-running. This is a convention, not something the source tells: long-running ads are usually the ones that work.

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

Required. Google blocks a single IP after a few dozen requests, so every request goes through a rotating proxy. Only Apify's Residential group gets through: datacenter IPs are refused by the Transparency Center, and the Google SERP and Unblocker groups cannot reach it at all, so any other Apify proxy group is switched to Residential for the run, with a note in the log. Your own proxy URLs are used as given.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "hubspot.com"
  ],
  "maxAdvertisersPerQuery": 3,
  "adFormat": "all",
  "platform": "all",
  "fastPeriodFilter": false,
  "maxAdsPerTarget": 200,
  "adDetails": false,
  "ocrTextAds": false,
  "enrichWorkers": 6,
  "longRunnerDays": 30,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One row per ad creative, with its status columns.

## `tables` (type: `string`):

Links to the targets table (status of every domain, advertiser or brand), the competitor report and the messages table.

# 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 = {
    "domains": [
        "apify.com",
        "hubspot.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("firsthand/google-ads-transparency-report").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 = {
    "domains": [
        "apify.com",
        "hubspot.com",
    ],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("firsthand/google-ads-transparency-report").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 '{
  "domains": [
    "apify.com",
    "hubspot.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call firsthand/google-ads-transparency-report --silent --output-dataset

```

## MCP server setup

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

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/gua9CWdFgUMsbrP9N/builds/CRBl26CbqjdSRsrtT/openapi.json
