# Ad Intel: Google, Meta & TikTok Ad Library Monitor (`chaptersoft/ad-intel-unified-ad-library`) Actor

One input → Google, Meta & TikTok ad libraries (LinkedIn: beta, opt-in). Resolves brands/domains, unified schema, new/changed/stopped diffs. Pay per ad only — no start fee.

- **URL**: https://apify.com/chaptersoft/ad-intel-unified-ad-library.md
- **Developed by:** [Jeremy Lam](https://apify.com/chaptersoft) (community)
- **Categories:** Marketing, Social media, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 google 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

## Ad Intel: Google, Meta & TikTok Ad Library monitor

Enter a competitor's **website, brand name or page URL**. In one run you get **every ad they show** on the **Google Ads Transparency Center** (Search, YouTube, Display, Maps, Play, Shopping), the **Meta Ad Library** (Facebook, Instagram, Messenger, Audience Network, Threads), the **TikTok Ads Library** (ads shown in the EU/EEA, Switzerland and the UK) and, experimentally, the **LinkedIn Ad Library**. Everything comes back in **one clean, de-duplicated schema**, and each ad is flagged as **new, changed or stopped since your last run**.

No login. No cookies. No API keys. You pay only for the ads you get.

> **LinkedIn (beta): off by default, experimental, currently blocked by LinkedIn.** LinkedIn's Ad Library currently answers our requests with HTTP 403, so LinkedIn usually returns no ads (and costs nothing, because you pay only per ad). It is not searched unless you add `"linkedin"` to **Platforms**; if you do, the run still succeeds and `OUTPUT.errors` reports the block. Google, Meta and TikTok are not affected.

### Why this Actor

| | Ad Intel | Typical ad-library Actor |
|---|---|---|
| Platforms per run | **Google + Meta + TikTok** (LinkedIn: beta, opt-in, currently blocked by LinkedIn) | One |
| Input | `nike.com`, `Nike`, a page URL, a page ID, an AR… ID, a LinkedIn company URL or any library URL | Usually a library URL or page ID |
| Advertiser matching | Resolves the domain or brand to the right Facebook page, the Google advertiser(s) **verified by the domain their ads link to** (e.g. `notion.so` → *Notion Labs, Inc*), the LinkedIn company and the TikTok advertiser. Look-alikes are dropped, and a **"did you mean"** list goes to `OUTPUT` | Keyword results with other advertisers mixed in |
| EU transparency (DSA) | Meta: EU reach plus **age × gender × country reach**, targeting (age, gender, locations), payer/beneficiary. TikTok: **impressions by country × age × gender**, targeting, audience size. LinkedIn: impressions by country, targeting | Usually missing, or a separate paid add-on |
| Google text ads | Headline, description, display URL, sitelinks, local-ad business/category/address. Ads published only as images are read with **OCR** | Raw preview strings or nothing |
| "What's new?" | Built-in diff: `new`, `changed` (with the changed fields), `unchanged`, `stopped` | Re-scrape everything and diff it yourself |
| Cost control | Hard cap per advertiser and platform, charge per ad only, no start fee, stops at your max charge | Issues report runs that ignored limits |
| Output | One schema plus optional `raw` platform record, and 3 ready-made table views | Different field names per platform |

### Quick start

```json
{
  "queries": ["nike.com", "notion.so", "https://www.facebook.com/nike"],
  "platforms": ["google", "meta", "tiktok"],
  "countries": ["US"],
  "maxAdsPerPlatform": 50
}
```

Schedule this input daily with **Output = "Only ads that are new since last run"**, then attach a webhook, Slack or email integration. You get an alert whenever a competitor launches a new creative.

#### Accepted queries

| You type | Google | Meta | LinkedIn (beta, opt-in) | TikTok |
|---|---|---|---|---|
| `nike.com` | ads for that domain + advertisers whose ads link to the brand's domains (verified) | resolves the Facebook page linking to nike.com | company whose name exactly matches "nike" | advertiser named "Nike" (TikTok advertiser search) |
| `Notion Labs` | advertiser-name lookup with domain verification | page-name match (or set *treat queries as keywords* for full-text search) | exact company-name match | advertiser search, keyword fallback optional |
| `AR16735076323512287233` / `adstransparency.google.com/advertiser/AR…` | that advertiser | – | – | – |
| `https://www.facebook.com/nike` or `15087023444` | – | that page | – | – |
| `https://www.facebook.com/ads/library/?…` | – | that exact search, all filters kept | – | – |
| `https://www.linkedin.com/company/hubspot/` | – | – | that company (exact name) | – |
| `https://www.linkedin.com/ad-library/search?…` | – | – | that exact search | – |
| `https://library.tiktok.com/ads?…adv_biz_ids=…` | – | – | – | that advertiser |

### Output

Each dataset item is one ad. Fields that a platform doesn't publish are `null`, never invented.

| Field | Google | Meta | LinkedIn (beta) |
|---|---|---|---|
| `id` (`platform:adId`, stable) | ✓ | ✓ | ✓ |
| `advertiser.name / id / url` | ✓ | ✓ | ✓ |
| `advertiser.payer` (paid for by) | – | ✓ when shown | ✓ |
| `status` active/inactive | ✓ (last shown ≤2 days ago) | ✓ | ✓ when dates shown |
| `format` | text/image/video | image/video/carousel/… | image/video/carousel/document/… |
| `firstShown`, `lastShown`, `daysActive` | ✓ | ✓ | ✓ (EU-served ads) |
| `headline`, `body`, `description`, `cta` | parsed from the preview | ✓ | ✓ |
| `landingUrls`, `displayUrl` | display domain | ✓ | ✓ |
| `images`, `videos`, `thumbnail` | ✓ (YouTube links for video) | ✓ | ✓ |
| `variants` | ✓ | ✓ (collation / cards) | ✓ |
| `publisherPlatforms` | search/youtube/maps/play/shopping | facebook/instagram/… | linkedin |
| `countries` | ✓ | when Meta lists them | ✓ (impression share) |
| `impressions` (+ `byCountry`, per surface) | ✓ ranges | when shown (political/EU) | ✓ total + share by country |
| `spend` | – | ✓ (political/issue ads) | – |
| `reach.eu` | – | when shown | – |
| `targeting` | EU DSA flags (raw) | – | language, location, targeting parameters targeted/excluded |
| `change.status` (+ `changedFields`, `firstSeenAt`) | ✓ | ✓ | ✓ |
| `storedMedia` (with *Download images*) | ✓ | ✓ | ✓ |
| `raw` (with *Include raw*) | search + detail records | full Ad Library record | labels |

The run also writes an **`OUTPUT`** record to the key-value store. It holds counts per platform and per change status, the advertiser each query resolved to, each platform's timing and status, and any errors. Webhooks and agents can read that one record instead of the whole dataset.

#### Sample item (real output, trimmed)

```json
{
  "id": "meta:1107386451952079", "platform": "meta", "adId": "1107386451952079",
  "adLibraryUrl": "https://www.facebook.com/ads/library/?id=1107386451952079",
  "advertiser": { "name": "Nike", "id": "15087023444", "url": "https://www.facebook.com/nike/", "domain": "nike.com" },
  "status": "active", "format": "carousel", "firstShown": "2026-09-01", "lastShown": "2026-10-14", "daysActive": 46,
  "headline": "Nike Air Monarch IV", "body": "Run through summer with gear that can take the heat.", "cta": "Shop now",
  "landingUrls": ["https://www.nike.com/"], "publisherPlatforms": ["facebook", "instagram"], "variants": 6,
  "change": { "status": "new", "firstSeenAt": "2026-10-15T12:41:15.171Z" }
}
```

### Monitoring and diffs

- History lives in a **named key-value store** (`ad-intel-monitor` by default). Apify never auto-deletes named stores.
- By default the **monitor name** is derived from your queries, platforms, countries, status and media type. The same input always compares against its own history. Set *Monitor name* to keep separate watchlists.
- `changed` ignores noise. A moving `lastShown` date and rotating signed CDN URLs don't count as changes. For Meta dynamic product ads (`isDynamic: true`), only non-creative fields are compared, because Meta rotates their cards on every load.
- `stopped` is reported **only when a platform was scraped completely** for that query. A capped or failed run never produces false "stopped" alerts. Stopped notices are **free**.

### Pricing (pay per event, no start fee)

| Event | Price | Example |
|---|---|---|
| Google ad (with impressions, surfaces, text) | $1.00 / 1,000 | 50 ads ≈ $0.05 |
| Meta ad | $2.00 / 1,000 | 50 ads ≈ $0.10 |
| LinkedIn ad (with detail page; experimental, currently blocked by LinkedIn) | $2.00 / 1,000 | 50 ads ≈ $0.10 |
| TikTok ad (with age/gender/country detail) | $3.00 / 1,000 | 50 ads ≈ $0.15 |
| Stopped-ad notices, unchanged ads hidden by the output mode | free | |

**Max cost ≈ queries × platforms × max ads per platform × price.** The Actor stops cleanly when it reaches the run's max-charge limit.

Google, Meta and TikTok connect directly (no proxy) and switch to Apify datacenter proxy only if blocked. Residential proxy is used for LinkedIn only. This keeps per-ad prices low and stable.

### Use with AI agents and MCP

- Pricing is per event only, and the Actor needs only limited permissions (it writes only to its own storages and the named monitor store), so agents can call it on a fixed budget.
- Call it through the Apify MCP server (`https://mcp.apify.com`) as `ad-intel-unified-ad-library`, or with `POST https://api.apify.com/v2/acts/<user>~ad-intel-unified-ad-library/run-sync-get-dataset-items`.
- Minimal agent input: `{"queries":["competitor.com"],"maxAdsPerPlatform":20,"outputMode":"new"}`. The flat JSON items need no post-processing.
- Read `OUTPUT` from the default key-value store for a one-object summary.

### Proxy and reliability

- **Google**: public Transparency Center RPC with no proxy. If Google starts rate-limiting, the Actor switches to your proxy on its own.
- **Meta**: real Chrome session on the public Ad Library page, direct connection first, Apify datacenter proxy as fallback. Meta throttles infinite-scroll pagination from data-center IPs, so the Actor reuses one browser session and loads the first page of up to ~40 filter slices (media type × platform × sort). Measured on Nike: 197 unique ads from 16 slices, against 30 from plain scrolling. EU detail calls are not throttled.
- **Google**: if Google rate-limits direct requests, the Actor switches to Apify Proxy when you have it, and otherwise to a real browser session, which still gets answers.
- **TikTok**: drives the public library UI in Chrome, because the API needs a page-generated signature. It paginates with *View more* and opens 3 ad-detail pages in parallel (about 2.4 s per ad with details).
- **LinkedIn** (experimental, currently blocked by LinkedIn: requests get HTTP 403): public Ad Library HTML with browser-grade headers, 2 parallel requests, and backoff that honours `Retry-After` on HTTP 429. If a detail page still fails, you still get the ad from its search card (`detailsMissing: true`).
- Every platform call retries with exponential backoff. One platform failing never fails the run; `OUTPUT.errors` explains what happened and how to fix it.

### FAQ

**Does it need my Facebook, Google or LinkedIn login?** No. It only reads public ad libraries.

**Why did Meta return fewer ads than the library shows?** Meta throttles infinite-scroll pagination from data-center IPs. The Actor widens coverage with filter slices (see above); for very large advertisers, split the run by country or media type. The log reports how many ads each step added.

**How are Google text ads parsed?** HTML previews are parsed by template: Search ads give headline, description, display URL and sitelinks; Local ads give business, category, address, headline and description. Ads published only as a rendered image are OCR'd (`extensions.textSource: "ocr"`, with `ocrConfidence`). Headlines that Google generates on the fly are flagged `extensions.dynamicHeadline: true` instead of being made up.

**LinkedIn shows no dates or impressions for some ads?** LinkedIn publishes dates, impressions and targeting for ads served in the EU (DSA). For other ads those fields are `null`.

**TikTok outside Europe?** TikTok publishes its ad library only for ads shown in the EU/EEA, Switzerland and the UK. If none of your countries is in that list, the Actor searches DE, FR, IT, ES and GB and says so in the log.

**Is this legal?** It collects only data the platforms publish in their public transparency libraries. Make sure your use complies with applicable laws and the platforms' terms.

# Actor input Schema

## `queries` (type: `array`):

One per line. Any mix of: website domain (nike.com), brand name (Notion Labs), Facebook page URL or page ID, Meta Ad Library URL, Google Ads Transparency advertiser URL or AR… ID, LinkedIn company URL or LinkedIn Ad Library URL (used only when LinkedIn is selected). Domains and names are searched on every selected platform; platform-specific URLs only on their platform.

## `platforms` (type: `array`):

Which ad libraries to search. Default: Google, Meta and TikTok. TikTok only publishes ads shown in the EU/EEA, Switzerland and the UK; if none of your countries are there, TikTok searches DE, FR, IT, ES and GB. LinkedIn is off by default: it is experimental and currently blocked by LinkedIn (its Ad Library answers HTTP 403), so it usually returns no ads. Select it only if you want to test it.

## `countries` (type: `array`):

ISO 3166-1 alpha-2 codes (US, GB, DE, FR, BR…) or ALL. Meta runs one search per country and merges/dedupes them.

## `activeStatus` (type: `string`):

Return currently running ads, stopped ads, or both.

## `mediaType` (type: `string`):

Filter by creative type. Applied in the platform query where supported (Google, Meta).

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

Only ads shown/started on or after this date. Leave empty for no limit.

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

Only ads shown/started on or before this date.

## `maxAdsPerPlatform` (type: `integer`):

Hard cap that also caps your cost: max cost ≈ queries × platforms × this × price per ad.

## `outputMode` (type: `string`):

Every ad is compared against previous runs stored in a named key-value store. 'New only' is ideal for scheduled runs + webhooks/Slack alerts. You are only charged for ads written to the dataset.

## `monitorKey` (type: `string`):

Name for this watchlist's state. Leave empty to derive it from queries + platforms + countries, so the same input always compares with its own history. Use different names to keep separate histories.

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

Named store that keeps history between runs (named stores are never auto-deleted).

## `fetchDetails` (type: `boolean`):

Google: per-country impressions, surfaces, ad text from preview, video links. LinkedIn: impressions, targeting, payer. Turn off for maximum speed.

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

Many Google Search text ads are published only as a rendered snapshot image. On (default): read headline, description and display URL from that image with OCR (tagged extensions.textSource=ocr with a confidence score). English-trained, so non-Latin scripts are less reliable.

## `metaKeywordSearch` (type: `boolean`):

Off (default): brand names/domains are resolved to the advertiser's Facebook page so you get only their ads. On: full-text keyword search across all advertisers.

## `metaAdType` (type: `string`):

Meta Ad Library category. Political/issue ads include spend and EU reach data.

## `tiktokKeywordSearch` (type: `boolean`):

Off (default): only ads from the advertiser whose name matches your query (found via TikTok's advertiser search; a 'did you mean' list is written to OUTPUT otherwise). On: if no advertiser matches, return keyword matches (tagged matchType=keyword).

## `downloadMedia` (type: `boolean`):

Saves creatives to the run's key-value store (URLs from ad platforms expire). Adds storedMedia links to each ad.

## `downloadVideos` (type: `boolean`):

Also store video files (can be large). Requires "Download images to storage".

## `includeRaw` (type: `boolean`):

Adds the untouched platform record under raw (large).

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

Used for LinkedIn, which blocks datacenter IPs (Residential is the default). Google, Meta and TikTok always connect directly first and switch to Apify datacenter proxy automatically if blocked, so they never need Residential.

## `googleProxy` (type: `boolean`):

Google's public RPC usually works without proxy. Enable only if you see 429 errors; uses Apify datacenter proxy (or your own proxy URLs).

## Actor input object example

```json
{
  "queries": [
    "nike.com",
    "https://www.facebook.com/notionhq",
    "Notion Labs"
  ],
  "platforms": [
    "google",
    "meta",
    "tiktok"
  ],
  "countries": [
    "US"
  ],
  "activeStatus": "active",
  "mediaType": "all",
  "maxAdsPerPlatform": 20,
  "outputMode": "all",
  "stateStoreName": "ad-intel-monitor",
  "fetchDetails": true,
  "ocrTextAds": true,
  "metaKeywordSearch": false,
  "metaAdType": "all",
  "tiktokKeywordSearch": false,
  "downloadMedia": false,
  "downloadVideos": false,
  "includeRaw": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "googleProxy": false
}
```

# Actor output Schema

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

One item per ad in the unified schema: platform, advertiser, status, format, headline, body, CTA, landing URLs, first/last shown, countries, impressions/spend/reach where published, and the change status since the last run.

## `newAds` (type: `string`):

The same dataset shown with the 'New since last run' view (creative, platform, advertiser, change status, text, landing URLs, first shown).

## `performance` (type: `string`):

The same dataset shown with the reach/spend/impressions view.

## `summary` (type: `string`):

JSON summary of the run: monitor key, per-query/per-platform ad counts and status, totals of new/changed/unchanged/stopped ads, 'did you mean' advertiser suggestions and any platform errors with fixes.

## `files` (type: `string`):

Key-value store with ad images (and videos if enabled) saved as <platform>-<adId>-<n>.<ext>; each ad's storedMedia field links to its files. Also holds the OUTPUT summary.

# 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 = {
    "queries": [
        "nike.com",
        "notion.so"
    ],
    "platforms": [
        "google",
        "meta",
        "tiktok"
    ],
    "countries": [
        "US"
    ],
    "maxAdsPerPlatform": 20,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("chaptersoft/ad-intel-unified-ad-library").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 = {
    "queries": [
        "nike.com",
        "notion.so",
    ],
    "platforms": [
        "google",
        "meta",
        "tiktok",
    ],
    "countries": ["US"],
    "maxAdsPerPlatform": 20,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("chaptersoft/ad-intel-unified-ad-library").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 '{
  "queries": [
    "nike.com",
    "notion.so"
  ],
  "platforms": [
    "google",
    "meta",
    "tiktok"
  ],
  "countries": [
    "US"
  ],
  "maxAdsPerPlatform": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call chaptersoft/ad-intel-unified-ad-library --silent --output-dataset

```

## MCP server setup

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

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/QmQu5JHzyT7DZucKi/builds/42UMDktZTcjv6P1Gk/openapi.json
