# EU Ad Library Monitor: Meta & Google Competitor Ads (`olliolli/meta-ad-library-eu`) Actor

Track Facebook, Instagram and Google competitor ads in one dataset. Monitor new creatives and observed changes, skip unchanged ads for free, and add an AI summary. EU country filters, clear coverage reports, no login.

- **URL**: https://apify.com/olliolli/meta-ad-library-eu.md
- **Developed by:** [Tom J](https://apify.com/olliolli) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 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.

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

## EU Ad Library Monitor: Meta & Google Competitor Ads

Follow **competitor ads** in one dataset. This public-route **Facebook Ad Library scraper** collects Facebook and **Instagram ads** alongside Google Ads Transparency observations, compares them with your previous run, and returns new or changed records. No Facebook or Google login, account cookies, developer account or identity verification.

**Built for bounded competitor monitoring.** Choose a country and an ad limit, then inspect the source-coverage report. A successful run can contain a useful partial sample; it is not a promise that every advertisement on the internet was found.

### Facebook Ad Library monitoring for EU competitors

- Agencies following a named list of EU competitors.
- Brands comparing public Meta and Google advertising observations.
- Analysts who want new creative observations and explicit changes instead of repeated dumps.
- Workflows in Google Sheets, Make, n8n or an internal reporting tool.

The first monitor run establishes a baseline from the ads actually retrieved. Later runs omit unchanged ads. **New means newly observed by this monitor**, not necessarily a campaign launched this week. A larger limit can reveal older advertisements that the monitor has never seen before.

### Quick start: a Benelux competitor watchlist

```json
{
  "watchlist": [
    {
      "name": "Coolblue BE",
      "metaPageId": "1809566712696587",
      "googleDomain": "coolblue.be"
    }
  ],
  "country": "BE",
  "mode": "monitor",
  "stateName": "benelux-competitors",
  "maxAdsPerSource": 20,
  "maxPagesPerSource": 3,
  "includeEuDetails": false,
  "includeDigest": false
}
```

1. Enter a competitor label, its numeric Meta advertiser page ID and/or its Google advertiser domain.
2. Select the EU country where you want to observe advertising.
3. Start with a small cap. Inspect both the dataset and `OUTPUT`.
4. Reuse the same monitor name for subsequent comparisons. Schedule the saved task weekly in `Europe/Brussels` if you want weekly observations.

Each platform identifier is optional, but every competitor needs at least one. The Actor deliberately uses exact advertiser identifiers rather than guessing which same-name Facebook page belongs to a business. A domain can match multiple Google advertiser entities; their IDs remain separate in the output. A watchlist label groups the identifiers you supplied; it is not independently verified corporate ownership.

Changing the country or source identifier creates a separate baseline. Changing the display label does not. Use separate monitor names for separate projects. Avoid overlapping runs with the same name.

#### How changes work

| Output | Meaning | Result event |
|---|---|---|
| `ad` / `new` | This ad ID has not been delivered in this monitor scope | `ad` |
| `change` / `changed` | An observed ad's creative fields or explicit active status changed | `change` |
| `change` / `stopped` | Meta explicitly reports a previously active known ad as inactive | `change` |
| No row | The retrieved ad is unchanged | None |
| `digest` | A successfully generated summary of delivered observations, if enabled and configured | `digest` |

**Missing does not mean stopped.** Partial pages, result caps, changed rankings, rate limits and source delays can all hide ads. The monitor never creates a stop event solely because an ad disappears from a search. For previously active Meta ads not in the current sample, it can check one public inactive-results page and report up to 20 explicit inactive matches. This is a bounded check, not a guarantee of detecting every stopped advertisement.

Google's `lastShown` is an observation date, not a reliable live campaign switch. Google ads therefore have `isActive: null`; this Actor does not infer Google stop events from an old date or missing search result.

Measurement growth and last-seen dates do not create creative-change events. Expiring media query signatures and CDN host changes are excluded from the creative comparison. Image paths can still change when the source republishes a file.

Use `mode: "snapshot"` when you want each run's retrieved observations, including known ads. Snapshot mode charges for delivered ads again; monitor mode skips unchanged ones.

#### Input controls

| Field | Default | Purpose |
|---|---|---|
| `watchlist` | One Coolblue BE example | 1–10 competitors, with explicit source identifiers |
| `country` | `BE` | One EU country per run |
| `mode` | `monitor` | Change-only observations or a fresh snapshot |
| `stateName` | `eu-ad-watch` | Persistent comparison namespace |
| `maxAdsPerSource` | 20 | 1–200 active/search observations per competitor per platform |
| `maxPagesPerSource` | 3 | At most 20 pages per connection attempt; automatic recovery can make a second bounded attempt |
| `maxRunSeconds` | 150 | Internal processing limit, 30–240 seconds |
| `connection` | `auto` | Datacenter first, at most one residential recovery session; or choose either route explicitly |
| `includeEuDetails` | false | Optional public transparency details; availability varies |
| `includeDigest` | false | Optional AI summary, billed only when delivered; no customer AI key needed |

The standard memory is 1 GB. Meta uses an anonymous browser and the same public requests as its website. With `connection: "auto"`, a blocked or rate-limited datacenter read may trigger **one residential session for the whole run**, reused for the remaining competitors. There is no third session or repeated IP rotation. Successful partial observations are retained if recovery fails. `connectionAttempts` and `recoveryEvents` show what happened. Residential recovery has no separate customer surcharge at the listed event prices.

Google uses public website requests and may make one datacenter fallback after a direct 302/403 response. The Actor never recovers a login or CAPTCHA by signing in or solving it. A failed Meta source does not make successful Google observations complete for both platforms.

#### A shared output format

Both platforms use `platform`, `advertiserId`, `creativeId`, `advertiserName`, `domain`, `format`, `firstShown`, `lastShown`, `previewUrl` and `imageUrl`, matching the core names of our Google Ads Transparency Actor. Additional monitoring fields include:

- `recordType`, `changeType`, `competitor`, `country`, `observedAt`, `previousObservedAt`.
- `isActive`, when explicitly available, and `adUrl`.
- `title`, `text`, `cta`, `landingPage`, creative `cards` and media links where the source provides them.
- `publisherPlatforms`, `tags`, `euDetails` and `euDetailsStatus`.
- `sourceComplete`, which describes the source scan rather than the entire real-world advertising account.

Google does not provide all the creative-text fields through this adapter; missing values remain null. Media are links, not archived copies, and can expire. No image or video files are downloaded or rehosted.

#### EU data: keep the metrics separate

With `includeEuDetails: true`, Meta detail rows can contain `euTotalReach`, `ageCountryGenderReach`, `beneficiaryPayers` and `audienceSelection`. The public browser route returned all of these for three Belgian test ads. They are source-reported metrics, not our estimates of campaign performance. Audience-selection age/gender describes the advertised audience criteria; the country/age/gender reach breakdown describes reported reach. Do not treat them as the same measurement.

Availability differs by ad and source. Missing details remain null with an explicit status. Language labels are not provided by the current Meta adapter and remain an empty list. No guarantee that every ad exposes every transparency field.

Google can expose country-specific **times-shown intervals** and audience-selection categories. Those are not the same as Meta's reach or a breakdown of actual viewers. Google platform codes are retained where their meaning is not verified. The source country's presence in a search does not prove the ad was targeted exclusively there.

This product does not estimate advertising spend, clicks, conversions, profitability or return on advertising spend. It does not invent missing impressions or infer campaign success from an ad's age.

### Pricing: pay for delivered observations and changes

Pay-per-event prices:

| Event | Price | Charged when |
|---|---:|---|
| `ad` | $0.0015 — $1.50 per 1,000 | One new delivered observation in monitor mode, or one delivered ad in snapshot mode |
| `change` | $0.003 | One delivered changed/stopped record for a known ad |
| `digest` | $0.05 | One successful delivered AI digest |

A newly observed ad receives only the ad event, not an additional change event. Unchanged ads, duplicates, failed pages, unavailable detail lookups and failed digests receive no result event. There is no automatic dataset-item event. A standard start event costs $0.00005 per allocated GB, minimum one, including failed runs. At the default 1 GB this is $0.00005 per run. Confirm the live Pricing tab before running.

Examples at the default 1 GB, including the $0.00005 start event:

- First baseline with 100 delivered ads: **$0.15005**.
- Later run with 10 new ads and 3 changes: **$0.02405**.
- The same run plus one successful digest: **$0.07405**.
- A monitor run containing only unchanged observations: **$0.00005 start only; no ad or change charges**.

Set a maximum charge per run in Apify Console. The Actor checks affordability before storing a billable row. If a delivery is interrupted between storage, charging and checkpointing, it stops for review on resurrection rather than blindly replaying a possibly charged event. Exactly-once delivery across every crash or concurrent run is not guaranteed.

#### Optional digest and tags

Offer and CTA tags are simple rules, not an AI judgment of performance. An optional AI digest summarizes the observations delivered in that run, with supporting ad IDs. Exact counts and coverage are supplied separately by the application. Schedule the task weekly for a weekly digest; it does not independently reconstruct a full calendar week's unseen activity.

The AI feature is disabled by default. AI processing is included in the digest price; customers do not need their own AI API key. It passed a small live Apify test with three public Meta ads and one delivered summary; this does not establish reliability across every advertiser. No digest is charged when configuration is missing or generation fails. If enabled, selected public ad text is processed by Cloudflare Workers AI. Treat summaries as analytical assistance, not verified statements about a competitor's strategy.

#### Check coverage, not just the green run status

`OUTPUT` includes `adsDelivered`, `changesDelivered`, `digestsDelivered`, `unchangedAds`, `sources`, `partial`, `stopped`, `uncheckedSources`, `warnings` and `chargedEvents`.

For each source, check `complete`, `status`, `warnings`, `adsObserved`, `pagesFetched` and `sourceTotalEstimate`. Meta detail diagnostics separately count `detailsRequested`, `detailsAvailable`, `detailsUnavailable` and `detailsNotChecked`; complete ad-list coverage does not imply complete transparency details. The source total can be approximate. A partial run may contain useful ads while still being unsuitable as a complete inventory. A blocked response is never interpreted as zero advertising activity.

Monitoring stores bounded identifiers, fingerprints and timestamps in a named Apify key-value store. It retains at most 10,000 known ads per source scope, then reports a capacity stop instead of silently forgetting IDs. Run-level rows stay in your Apify dataset according to your storage settings.

#### Privacy and scope

This Actor collects public advertiser and advertisement data. It does not collect viewer identities, user profiles, comments, likes, profile photos, administrator contact details, email lists or telephone lists. Known contact patterns in ad text are redacted and landing-page tracking queries are stripped.

Public advertiser/payer names may identify a sole trader, and ad copy or creative media can contain people or names. This is **not guaranteed anonymization**. Review the public content before sharing or combining it with other data. Historic private probe runs can contain raw source responses; the production monitor does not store raw HTML or full profile objects.

#### API and integrations

Run `olliolli/meta-ad-library-eu` with the Apify API or save the input as an Apify task. Export the default dataset as JSON, CSV or Excel; read the `OUTPUT` key-value record alongside it. Connect the dataset to Make, n8n, Zapier or Google Sheets using Apify integrations.

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("olliolli/meta-ad-library-eu").call(run_input={
    "watchlist": [{"name": "Coolblue BE", "metaPageId": "1809566712696587",
                   "googleDomain": "coolblue.be"}],
    "country": "BE", "mode": "monitor", "stateName": "benelux-weekly",
    "maxAdsPerSource": 20
})
coverage = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")
rows = client.dataset(run["defaultDatasetId"]).list_items().items
```

### Frequently asked questions

**Do I need Facebook login, cookies or identity verification?** No. Those inputs are not supported. If the public route is blocked, the source is reported as unavailable.

**Does this monitor all of a competitor's ads?** Only the ads available through the selected public sources, country, limits and successful pages. No complete-account guarantee.

**Why did I get no new rows?** The retrieved ads may be unchanged, or a source may have failed. Check `unchangedAds` and the source reports to distinguish them.

**Can I compare Meta and Google reach directly?** No. They report different metrics and coverage. Preserve platform and metric labels.

**Can a missing ad be reported as stopped?** Not from absence alone. Meta stop events require explicit inactive status; Google stop detection is not inferred.

**Is this an official Meta or Google API?** No. It is an independent tool using public website responses, with no affiliation or endorsement. Public website availability and structure can change.

For support, provide the Apify run ID, the source identifier, country and expected outcome. Do not send passwords, login cookies or account access tokens.

### Related tools

Use [Google Ads Transparency: EU Reach & Ads](https://apify.com/olliolli/google-ads-transparency-eu) for a dedicated Google advertising workflow, or [Review Digest](https://apify.com/olliolli/review-digest) for customer-review summaries and reply drafts. Review Digest analyzes reviews; this Actor summarizes advertising observations.

Public source interfaces: [Meta Ad Library](https://www.facebook.com/ads/library/) and [Google Ads Transparency Center](https://adstransparency.google.com/).

# Actor input Schema

## `watchlist` (type: `array`):

1–10 competitors with name, optional numeric metaPageId and optional googleDomain. Example: \[{"name":"Coolblue BE","metaPageId":"1809566712696587","googleDomain":"coolblue.be"}]. Exact identifiers avoid unrelated namesakes.

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

Country in which the ads were shown.

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

Monitor: only new and changed observations. Snapshot: charge delivered ads each run. Missing ads alone are never reported as stopped.

## `stateName` (type: `string`):

Reuse with the same watchlist and country. Avoid overlapping runs.

## `maxAdsPerSource` (type: `integer`):

Bounded observations per competitor and platform; smaller limits may produce partial scans.

## `maxPagesPerSource` (type: `integer`):

Maximum source pages per connection attempt. Automatic Meta recovery may make one second bounded attempt; OUTPUT reports the total.

## `maxRunSeconds` (type: `integer`):

Internal time limit before a clean stop.

## `includeEuDetails` (type: `boolean`):

Google details supported. Meta demographic mapping is not yet validated; unavailable fields remain empty.

## `includeDigest` (type: `boolean`):

Only successful AI summaries are charged. Currently unavailable until the owner configures and validates Workers AI.

## `connection` (type: `string`):

Automatic uses at most one datacenter and one residential Meta session per run. No further IP rotation, logins or captcha handling.

## Actor input object example

```json
{
  "watchlist": [
    {
      "name": "Coolblue BE",
      "metaPageId": "1809566712696587",
      "googleDomain": "coolblue.be"
    }
  ],
  "country": "BE",
  "mode": "monitor",
  "stateName": "eu-ad-watch",
  "maxAdsPerSource": 20,
  "maxPagesPerSource": 3,
  "maxRunSeconds": 150,
  "includeEuDetails": false,
  "includeDigest": false,
  "connection": "auto"
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `summary` (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 = {
    "watchlist": [
        {
            "name": "Coolblue BE",
            "metaPageId": "1809566712696587",
            "googleDomain": "coolblue.be"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("olliolli/meta-ad-library-eu").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 = { "watchlist": [{
            "name": "Coolblue BE",
            "metaPageId": "1809566712696587",
            "googleDomain": "coolblue.be",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("olliolli/meta-ad-library-eu").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 '{
  "watchlist": [
    {
      "name": "Coolblue BE",
      "metaPageId": "1809566712696587",
      "googleDomain": "coolblue.be"
    }
  ]
}' |
apify call olliolli/meta-ad-library-eu --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,olliolli/meta-ad-library-eu"
        }
    }
}
```

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/Ei43Per6vYsI95Tda/builds/zKlnTUzTAmnl9SrIu/openapi.json
