# Google Ads Scraper - Ad Creatives, Images & Audience Targeting (`smart-digital/google-ads-transparency-scraper`) Actor

Scrape every ad an advertiser runs from Google's Ads Transparency Center: one row per creative with its archived image, format, serving dates, countries, ad wording where Google keeps it readable, and EU audience targeting. Creatives Google holds no content for are never returned.

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

## Pricing

$1.20 / 1,000 creative delivereds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

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

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

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

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

# README

## Google Ads Scraper — every ad an advertiser runs, from the Ads Transparency Center

Reads the public [Google Ads Transparency Center](https://adstransparency.google.com) and returns
one row per ad creative an advertiser has run, with the content Google actually holds for it: the
archived image, the ad wording where Google keeps it readable, the serving period, the countries it
ran in, and the audience targeting the advertiser had to disclose in Europe.

### The one rule this actor follows

**It never returns a row it cannot fill, and never writes a value it does not have.**

Google's registry hands back a share of creatives it holds no image and no preview for. Returning
them produces a dataset of identifiers and dates that cannot be used for anything — and you pay for
them all the same. Here they are counted in the run summary and dropped.

Three measurements behind that sentence, all reproducible on your own runs:

| | Measured |
|---|---|
| Creatives returned with no image and no preview | **0 out of 140** delivered, across two advertisers |
| `adText` filled with a placeholder, a template or a fragment of markup | **0** — the field is either real ad wording or `null` |
| `audienceTargeting` returned as an empty object where Google published nothing | **0** — it is `null`, which does not mean "targeted nobody" |

Same rule everywhere else: a column the registry never fills is not shipped at all rather than
shipped empty. The exact placement (`STATIC_GOOGLE_SEARCH`, `DYNAMIC_MAPS_PROMOTED_PIN` and 68
others) is declared by the registry but never filled on the public endpoint — checked on 60 search
results and 4 detail lookups — so this actor does not offer a placement column.

### What you get

One dataset item per creative:

| Field | What it is |
|---|---|
| `advertiserId`, `advertiserName` | The advertiser as the registry names it |
| `creativeId` | Google's identifier for this creative |
| `domain` | The domain the ad points to |
| `format` | `TEXT`, `IMAGE` or `VIDEO` |
| `imageUrl`, `imageWidth`, `imageHeight` | Direct URL of the archived image, when the creative has one — a downloadable visual, not a page to render |
| `previewScriptUrl` | How to render the creative, when it is served as a rendered preview instead of a still image |
| `adText` | The wording of the ad, when Google keeps it as text rather than as a picture. `null` otherwise, never a placeholder |
| `displayUrl` | The URL shown in the ad, when the creative renders one |
| `advertiserDisplayName` | The name shown in the ad, often more specific than `advertiserName` |
| `adTextParts` | Every string the creative renders, in order, so nothing is lost by the mapping above |
| `firstShown`, `lastShown`, `daysServed` | Serving period, as `YYYY-MM-DD` |
| `politicalRegions` | Regions where the ad is registered as political, when applicable |
| `advertiserUnverified`, `reportedByUser` | Flags the registry sets on the creative |
| `creativeUrl` | Link back to the creative's page in the registry |

#### Sample output

A real row, shortened (a text ad Booking.com ran across 11 countries):

```json
{
  "advertiserId": "AR02934798844673654785",
  "advertiserName": "Booking.com",
  "creativeId": "CR04485766038394241025",
  "domain": "booking.com",
  "format": "TEXT",
  "imageUrl": null,
  "previewScriptUrl": "https://displayads-formats.googleusercontent.com/ads/preview/content.js?...",
  "adText": "Išsirinkite ir užsisakykite – geriausi kaimo turizmo sodybų pasiūlymai Lietuvoje...",
  "displayUrl": "www.booking.com/",
  "advertiserDisplayName": "Booking.com",
  "firstShown": "2026-08-19",
  "lastShown": "2026-09-06",
  "daysServed": 18,
  "advertiserUnverified": false,
  "creativeUrl": "https://adstransparency.google.com/advertiser/AR02934798844673654785/creative/CR04485766038394241025",
  "legalName": "Booking.com B.V.",
  "countriesServed": 11,
  "countries": ["IT", "ES", "HR", "LT", "PL", "CZ", "IE", "NO", "DE", "GR", "LV"],
  "countryDetails": [
    { "country": "BE", "geoCriteriaId": 2056, "firstServed": "2023-10-03", "lastServed": "2026-09-05",
      "impressionsLowerBound": 5000, "impressionsUpperBound": 6000, "dataAvailableFrom": null,
      "bySurface": [
        { "surface": "SEARCH", "impressionsLowerBound": 5000, "impressionsUpperBound": 6000 },
        { "surface": "YOUTUBE", "impressionsLowerBound": null, "impressionsUpperBound": 1000 }
      ] }
  ],
  "eeaImpressions": {
    "impressionsLowerBound": 1250000, "impressionsUpperBound": 1500000,
    "firstServed": "2023-10-02", "lastServed": "2026-09-05", "dataAvailableFrom": null,
    "bySurface": [
      { "surface": "SEARCH", "impressionsLowerBound": 1250000, "impressionsUpperBound": 1500000 },
      { "surface": "YOUTUBE", "impressionsLowerBound": 25000, "impressionsUpperBound": 30000 }
    ]
  }
}
```

#### With per-country details switched on

`includeDetails` adds one lookup per creative and fills six more fields:

| Field | What it is |
|---|---|
| `legalName` | The legal entity behind the advertiser — `Booking.com B.V.` where the search only says `Booking.com` |
| `countriesServed`, `countries` | Number of countries and their two-letter codes |
| `countryDetails` | Per country: Google's geo criteria ID, first and last served dates, the impression bracket where Google publishes one, and its split across Google properties. Sparse by nature — measured on 152 country rows, a first-served date on 59 and an impression bracket on 30 |
| `eeaImpressions` | How many times the ad was shown across the EEA, as a bracket, split by Google property. See below |
| `variantCount`, `variantUrls` | Every rendering of the creative, where the search returns only one (3 to 4 on the videos tested) |
| `audienceTargeting`, `audienceTargetingSignals` | See below |

#### Audience targeting, and what it really is

Google publishes this because the EU Digital Services Act requires it, which is what decides its
coverage: **creatives served in the European Economic Area carry it, the others do not.** Measured
on 32 creatives — 8 of 8 for a French advertiser, 7 of 12 for `booking.com`, 2 of 12 for
`salesforce.com`. Where Google publishes nothing, the field is `null`.

Five signals, each with what the advertiser included and what it excluded: `demographics`,
`geography`, `contextual`, `topicOfInterest`, `advertiserList`.

```json
"audienceTargeting": {
  "demographics": { "included": true, "excluded": false },
  "geography": { "included": true, "excluded": false },
  "contextual": { "included": true, "excluded": true },
  "advertiserList": { "included": true, "excluded": false }
},
"audienceTargetingSignals": ["demographics", "geography", "contextual", "advertiserList"]
```

The registry also declares age, gender and geographic conditions next to these signals. They are
never filled on the public endpoint, so nothing is shipped for them.

#### How many times the ad was shown

`eeaImpressions` is the volume the ad reached across the European Economic Area, plus its split
between the Google properties it ran on: `SEARCH`, `YOUTUBE`, `MAPS`, `PLAY`, `SHOPPING`. Same
disclosure duty as the targeting above, so it lands on exactly the same creatives — measured on 20
creatives across two advertisers, the two agree on all 20.

Google publishes brackets, never counts, and reading them takes two rules:

- **The two bounds are independent.** `{ "impressionsLowerBound": null, "impressionsUpperBound":
  1000 }` is Google saying *fewer than a thousand*. It is a value, not a gap.
- **A recent ad has no bracket yet, and says when it will.** `dataAvailableFrom` carries the future
  date Google will publish the figures on. Rather than an empty cell with no explanation, you get
  the date to come back on. Measured: 13 of the 16 disclosed creatives had their bracket, the other
  3 a publication date.

The same six keys appear inside every `countryDetails` row, so the same volume is readable country
by country as well as EEA-wide.

### Input

| Field | Default | Notes |
|---|---|---|
| `searchTerm` | — | Required. A domain such as `booking.com`, or an advertiser name |
| `searchBy` | `domain` | Look up by the domain the ads point to, or by advertiser name |
| `maxResults` | `40` | Up to 1000. Results are paginated 40 at a time |
| `adFormat` | `any` | `text`, `image` or `video`. Filtering happens at the source, so formats you did not ask for are never fetched or charged |
| `regions` | `[]` | Google geo target IDs (`2250` France, `2840` United States, `2826` United Kingdom) |
| `servedFrom`, `servedTo` | — | Bounds on the serving period, `YYYY-MM-DD` |
| `includeDetails` | `false` | Adds the per-creative lookup described above |

```json
{
  "searchTerm": "booking.com",
  "maxResults": 40,
  "adFormat": "video",
  "includeDetails": true
}
```

A `RUN_SUMMARY` record is written to the run's key-value store: creatives delivered, how many
carried readable ad text, how many were skipped for having no content, pages fetched, and how many
times Google rate-limited the run.

### Limits, stated up front

- **Ad text is available on a minority of creatives, and the field is empty on the rest.** Google
  archives most ads as a picture of themselves, with the wording baked into the pixels; only the
  creatives it keeps as a rendered preview carry readable text. Measured across two advertisers,
  that is around 15% of creatives — but **every creative that carries a preview returns its text**,
  and asking for the per-creative details does not raise the share: on 40 creatives archived as
  images, none gained readable text from the extra lookup. Reading the rest would require OCR, which
  this actor does not do.
- **Audience targeting and impression volumes depend on where the ad ran**, for the reason given
  above. They are a European disclosure, not fields Google fills at random.
- **No exact placement**, for the reason given above.
- **No US impression volumes.** The registry declares the field next to the European one, and it
  came back empty on every creative tested. Nothing is shipped for it.
- **Rate limit.** The registry caps requests per IP: roughly 40 calls in a row, then every endpoint
  returns "too many requests" for a few minutes. Requests are spaced out and retried with a growing
  delay rather than failing the run, so large runs are slower rather than broken.
- **Per-country details are capped** at 200 creatives per run, because each one costs an extra
  request against that same rate limit.
- No proxy and no login: the registry answers a plain server IP.

Creatives Google holds no content for are neither returned nor charged.

# Actor input Schema

## `searchTerm` (type: `string`):

The advertiser's domain (for example booking.com) or, with searchBy set to advertiser, the advertiser name as it appears in the registry.

## `searchBy` (type: `string`):

Look the advertiser up by the domain its ads point to, or by its display name.

## `maxResults` (type: `integer`):

How many creatives to return at most. Results are paginated 40 at a time.

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

Restrict the search to one ad format. Text covers Search ads, image covers display banners, video covers YouTube and video placements.

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

Optional. Restrict to ads served in given regions, using Google geo target IDs (2250 France, 2840 United States, 2826 United Kingdom). Leave empty for anywhere.

## `servedFrom` (type: `string`):

Optional lower bound on the serving period, as YYYY-MM-DD.

## `servedTo` (type: `string`):

Optional upper bound on the serving period, as YYYY-MM-DD.

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

Fetches the legal advertiser entity and the per-country activity for each creative. Costs one extra request per creative against a rate-limited source, so it makes runs noticeably slower.

## Actor input object example

```json
{
  "searchTerm": "booking.com",
  "searchBy": "domain",
  "maxResults": 40,
  "adFormat": "any",
  "regions": [],
  "includeDetails": false
}
```

# Actor output Schema

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

Compact table: advertiser, domain, format, ad wording, creative image, serving dates.

## `allFields` (type: `string`):

Every field, including per-country serving detail, EEA impression volume and audience targeting when the run was started with details enabled.

# 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 = {
    "searchTerm": "booking.com"
};

// Run the Actor and wait for it to finish
const run = await client.actor("smart-digital/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 = { "searchTerm": "booking.com" }

# Run the Actor and wait for it to finish
run = client.actor("smart-digital/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 '{
  "searchTerm": "booking.com"
}' |
apify call smart-digital/google-ads-transparency-scraper --silent --output-dataset

```

## MCP server setup

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