# Facebook Ad Library Scraper & Competitor Ad Change Monitor (`kamerozkan/facebook-ad-library-change-monitor`) Actor

Scrape the active Facebook and Instagram ads of exact Pages from the Meta Ad Library, no login or API key. Track every verified ad change across runs: new, stopped, resumed and asset updates in webhook-ready JSON with ad copy, links, media and platforms.

- **URL**: https://apify.com/kamerozkan/facebook-ad-library-change-monitor.md
- **Developed by:** [Kamer Ozkan](https://apify.com/kamerozkan) (community)
- **Categories:** Social media, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 ad scanneds

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?

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

Track what changed in a competitor's Facebook and Instagram ads.

This Actor scrapes the active ads of exact Facebook Pages from the Meta Ad Library and tracks new, stopped, resumed, and changed ads across later runs. It is designed for competitor research, swipe-file exports, scheduled monitoring, agency reporting, and webhook automation.

You do not need a Facebook account, cookies, or a Meta Marketing API key. The Actor reads the same public Ad Library that anyone can open in a browser.

### What problem does it solve?

A normal Facebook Ad Library scraper downloads the same ad rows every day. Your workflow still has to decide what is actually new, what disappeared, and whether a failed or partial scrape created a false alarm.

This Actor keeps monitor history and returns a clean change feed:

- `NEW`: an ad appeared after the baseline and its own start date is on or after the previous accepted scan
- `DISCOVERED`: Meta returned a previously unseen ad whose start date predates the last accepted scan
- `MISSING_ONCE`: one tentative absence, available only when requested
- `STOPPED_CONFIRMED`: absent from two consecutive complete scans
- `RESUMED`: a previously stopped ad returned
- `ASSET_CHANGED`: the same Ad Library ID returned with different creative content

The first successful run creates the monitoring baseline. The default `ALL_CHECKED` output returns every current ad as a `CURRENT` row, while keeping existing ads out of the `NEW` event stream. Scheduled users can switch to `CHANGES_ONLY`.

### Why use this Actor?

#### Exact Page isolation, plus keyword feeds

The input accepts native Facebook Page IDs such as `15087023444`, Ad Library URLs that contain `view_all_page_id`, Page URLs such as `https://www.facebook.com/nike`, and Page usernames. Usernames are resolved to one exact Page ID before monitoring starts, so a Page monitor is never mixed with resellers, fan pages, or unrelated advertisers.

Keywords such as a product, brand, or competitor name are supported as separate targets. A keyword target returns the active ads Meta ranks for that term in the selected country, with the real Page ID and Page name on every row, and tracks which of those ads are new. Because keyword results mix many advertisers and change ranking over time, keyword targets always use the new-launch feed and never claim that an ad stopped.

#### Real ad copy, links, media, and platforms

Every row carries the ad's primary text, headline, destination URL, call-to-action, publisher platforms (Facebook, Instagram, Messenger, Audience Network, Threads), start date, format, and direct media URLs for images and videos, exactly as the Ad Library exposes them.

#### Safer stopped-ad detection

An ad is not marked stopped after one absence. `STOPPED_CONFIRMED` requires:

1. Two consecutive successful source runs
2. Complete pagination in both runs
3. The ad missing from both complete snapshots
4. At least six hours between the first absence and confirmation

Failed, blocked, timed-out, or capped scans do not advance the stopped-ad counter. Immediate retries cannot confirm a stopped ad.

Meta serves media from rotating CDN hosts with signed, expiring URL parameters. Those volatile parts are excluded from asset identity, so an unchanged creative never produces a false `ASSET_CHANGED` event. A different image, video, headline, or primary text still does.

#### Automatic large-advertiser protection

`AUTO` mode chooses the safest behavior for every Page:

- Complete scan: full lifecycle monitoring
- Result or page cap reached: new-launch feed only

The Actor never claims that an ad stopped when the Page was only partially scanned.

#### Changes-only output

Use `CHANGES_ONLY` for scheduled runs. The dataset stays empty when nothing changed, making it easy to connect the Actor to Slack, Discord, Make, Zapier, n8n, or your own webhook.

### Quick start

Use one exact Page ID for the first test:

```json
{
  "pageIds": ["15087023444"],
  "country": "ALL",
  "monitorName": "competitor-watch",
  "mode": "AUTO",
  "maxAdsPerPage": 300,
  "outputMode": "ALL_CHECKED"
}
```

The first run returns the current active ads and creates the baseline. Run it again with the same `monitorName`, Page IDs, and country to receive verified changes.

### Example output

The `CURRENT` record is unedited output from production run `Pb1C64QubSd3Wkfjz` (Nike, Germany) on 2026-09-02. That run finished `HEALTHY`, exhausted all six Ad Library result pages, accepted the lifecycle comparison, and charged 60 ad scans and zero change events. The stopped-ad sequence is an explicitly labeled deterministic replay of the same live snapshot that demonstrates both confirmation stages. Signed media URL parameters are shortened to `?...` for readability only.

#### 1. A checked ad (`CURRENT`)

One ad, one Page, one record. Fields the Ad Library does not publish for this ad stay `null` inside `current`; nothing is guessed.

<details>
<summary>Show the full JSON example</summary>

```json
{
  "recordType": "CURRENT",
  "status": "CURRENT",
  "eventId": null,
  "eventType": null,
  "monitorName": "platform-smoke-res",
  "monitorKey": "platform-smoke-res:15087023444:DE:active",
  "modeUsed": "FULL_LIFECYCLE",
  "country": "DE",
  "advertiserId": "15087023444",
  "advertiserName": "Nike",
  "creativeId": "28093431340342783",
  "format": "VIDEO",
  "platforms": [
    "AUDIENCE_NETWORK",
    "FACEBOOK",
    "INSTAGRAM",
    "MESSENGER",
    "THREADS"
  ],
  "firstShown": "2026-08-10",
  "lastShown": "2026-09-02",
  "headline": "Finde deinen Flow",
  "body": "Vom ersten Training bis zum Schlusspfiff: Entdecke Basketball-Essentials für einen starken Auftritt bei jeder Partie.",
  "destinationUrl": "https://fb.com/canvas_doc/1678531143227828",
  "primaryAssetUrl": "https://scontent.fbne11-1.fna.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
  "sourceUrl": "https://www.facebook.com/ads/library/?id=28093431340342783",
  "assetUrls": [
    "https://scontent.fbne11-1.fna.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
    "https://video.fbne11-1.fna.fbcdn.net/o1/v/t2/f2/m366/AQPSADM-Enxo3uSlcLMidGPDSWBtXcDQ1m2ppKuyoQj2i17yj7QKhxvkoRjy5rsJjs8sVtLCxTyNUCDFou2m4q4F5Lbcijd7waYLnBYikH8cOg.mp4?...",
    "https://video.fbne11-1.fna.fbcdn.net/o1/v/t2/f2/m412/AQN0CU95pKgN-QCGEacKjDWf4SpH6xBIY6AFnqONJywgMOJli2GRNoM3qb6ytN0NRVNyS45l-uJsoL1XzoIx1iVuys_3KRLaHfvmcIOKpQ.mp4?..."
  ],
  "previous": null,
  "current": {
    "advertiserId": "15087023444",
    "advertiserName": "Nike",
    "creativeId": "28093431340342783",
    "format": "VIDEO",
    "regions": [
      "DE"
    ],
    "firstShown": "2026-08-10",
    "lastShown": "2026-09-02",
    "headline": "Finde deinen Flow",
    "body": "Vom ersten Training bis zum Schlusspfiff: Entdecke Basketball-Essentials für einen starken Auftritt bei jeder Partie.",
    "destinationUrl": "https://fb.com/canvas_doc/1678531143227828",
    "platforms": [
      "AUDIENCE_NETWORK",
      "FACEBOOK",
      "INSTAGRAM",
      "MESSENGER",
      "THREADS"
    ],
    "ctaText": "Shop now",
    "ctaType": "SHOP_NOW",
    "collationCount": 3,
    "assetUrls": [
      "https://scontent.fbne11-1.fna.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
      "https://video.fbne11-1.fna.fbcdn.net/o1/v/t2/f2/m366/AQPSADM-Enxo3uSlcLMidGPDSWBtXcDQ1m2ppKuyoQj2i17yj7QKhxvkoRjy5rsJjs8sVtLCxTyNUCDFou2m4q4F5Lbcijd7waYLnBYikH8cOg.mp4?...",
      "https://video.fbne11-1.fna.fbcdn.net/o1/v/t2/f2/m412/AQN0CU95pKgN-QCGEacKjDWf4SpH6xBIY6AFnqONJywgMOJli2GRNoM3qb6ytN0NRVNyS45l-uJsoL1XzoIx1iVuys_3KRLaHfvmcIOKpQ.mp4?..."
    ],
    "primaryAssetUrl": "https://scontent.fbne11-1.fna.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
    "sourceUrl": "https://www.facebook.com/ads/library/?id=28093431340342783",
    "assetHash": "b6cd5862b7ebe46d03e30b19d826f2228c48a6c2694d83b3b2c0bdab16e1430b"
  },
  "evidence": {},
  "detectedAt": "2026-09-02T16:07:57.343Z",
  "schemaVersion": "1.0"
}
```

</details>

#### 2. How a stop is confirmed, never guessed

An ad disappearing from one scan is not a stopped ad. The engine reports it in two deliberate steps:

- **First absence is tentative.** `MISSING_ONCE` says the ad was absent from one complete, successful scan. `evidence.tentative` is `true` and nothing is confirmed.
- **Confirmation needs a second complete absence.** Only then does `STOPPED_CONFIRMED` fire, with the absence count, the first missing timestamp, and the confirmation interval in `evidence`.
- **Incomplete scans confirm nothing.** A partial or failed source run is rejected as `INCOMPLETE_SOURCE_RUN`: zero events, state untouched, no phantom stops.
- **The first observation of a monitor bills zero verified changes**, by a hard rule in the billing code, so starting a new monitor never charges for a wall of baseline events.

<details>
<summary>Show the MISSING_ONCE replay row</summary>

```json
{
  "recordType": "CHANGE",
  "status": "MISSING_ONCE",
  "eventId": "evt_624ea1d0fac2efe8efb2185f9ee0b6ce",
  "eventType": "MISSING_ONCE",
  "monitorName": "competitor-watch",
  "monitorKey": "competitor-watch:15087023444:DE:active",
  "modeUsed": "FULL_LIFECYCLE",
  "country": "DE",
  "advertiserId": "15087023444",
  "advertiserName": "Nike",
  "creativeId": "28093431340342783",
  "format": "VIDEO",
  "platforms": [
    "AUDIENCE_NETWORK",
    "FACEBOOK",
    "INSTAGRAM",
    "MESSENGER",
    "THREADS"
  ],
  "firstShown": "2026-08-10",
  "lastShown": "2026-09-02",
  "headline": "Finde deinen Flow",
  "body": "Vom ersten Training bis zum Schlusspfiff: Entdecke Basketball-Essentials für einen starken Auftritt bei jeder Partie.",
  "destinationUrl": "https://fb.com/canvas_doc/1678531143227828",
  "primaryAssetUrl": "https://scontent-fra3-1.xx.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
  "sourceUrl": "https://www.facebook.com/ads/library/?id=28093431340342783",
  "assetUrls": [
    "https://scontent-fra3-1.xx.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
    "https://video-fra3-1.xx.fbcdn.net/o1/v/t2/f2/m366/AQPSADM-Enxo3uSlcLMidGPDSWBtXcDQ1m2ppKuyoQj2i17yj7QKhxvkoRjy5rsJjs8sVtLCxTyNUCDFou2m4q4F5Lbcijd7waYLnBYikH8cOg.mp4?...",
    "https://video-fra3-1.xx.fbcdn.net/o1/v/t2/f2/m412/AQN0CU95pKgN-QCGEacKjDWf4SpH6xBIY6AFnqONJywgMOJli2GRNoM3qb6ytN0NRVNyS45l-uJsoL1XzoIx1iVuys_3KRLaHfvmcIOKpQ.mp4?..."
  ],
  "previous": {
    "advertiserId": "15087023444",
    "advertiserName": "Nike",
    "creativeId": "28093431340342783",
    "format": "VIDEO",
    "regions": [
      "DE"
    ],
    "firstShown": "2026-08-10",
    "lastShown": "2026-09-02",
    "headline": "Finde deinen Flow",
    "body": "Vom ersten Training bis zum Schlusspfiff: Entdecke Basketball-Essentials für einen starken Auftritt bei jeder Partie.",
    "destinationUrl": "https://fb.com/canvas_doc/1678531143227828",
    "platforms": [
      "AUDIENCE_NETWORK",
      "FACEBOOK",
      "INSTAGRAM",
      "MESSENGER",
      "THREADS"
    ],
    "ctaText": "Shop now",
    "ctaType": "SHOP_NOW",
    "collationCount": 3,
    "assetUrls": [
      "https://scontent-fra3-1.xx.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
      "https://video-fra3-1.xx.fbcdn.net/o1/v/t2/f2/m366/AQPSADM-Enxo3uSlcLMidGPDSWBtXcDQ1m2ppKuyoQj2i17yj7QKhxvkoRjy5rsJjs8sVtLCxTyNUCDFou2m4q4F5Lbcijd7waYLnBYikH8cOg.mp4?...",
      "https://video-fra3-1.xx.fbcdn.net/o1/v/t2/f2/m412/AQN0CU95pKgN-QCGEacKjDWf4SpH6xBIY6AFnqONJywgMOJli2GRNoM3qb6ytN0NRVNyS45l-uJsoL1XzoIx1iVuys_3KRLaHfvmcIOKpQ.mp4?..."
    ],
    "primaryAssetUrl": "https://scontent-fra3-1.xx.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
    "sourceUrl": "https://www.facebook.com/ads/library/?id=28093431340342783",
    "assetHash": "b6cd5862b7ebe46d03e30b19d826f2228c48a6c2694d83b3b2c0bdab16e1430b"
  },
  "current": null,
  "evidence": {
    "tentative": true,
    "completeSuccessfulRun": true,
    "runId": "competitor-watch:15087023444:DE:active:2026-09-03T04:00:00.000Z",
    "sourceEvidence": {
      "completenessReason": "PAGINATION_EXHAUSTED",
      "nextPageTokenPresent": false,
      "pageCount": 6,
      "approximateSourceCount": 60,
      "adStatus": "active",
      "transport": "replay"
    }
  },
  "detectedAt": "2026-09-03T04:00:00.000Z",
  "schemaVersion": "1.0"
}
```

</details>

<details>
<summary>Show the STOPPED_CONFIRMED replay row</summary>

```json
{
  "recordType": "CHANGE",
  "status": "STOPPED_CONFIRMED",
  "eventId": "evt_67cf6479e5d45145b12c0683d0faeec6",
  "eventType": "STOPPED_CONFIRMED",
  "monitorName": "competitor-watch",
  "monitorKey": "competitor-watch:15087023444:DE:active",
  "modeUsed": "FULL_LIFECYCLE",
  "country": "DE",
  "advertiserId": "15087023444",
  "advertiserName": "Nike",
  "creativeId": "28093431340342783",
  "format": "VIDEO",
  "platforms": [
    "AUDIENCE_NETWORK",
    "FACEBOOK",
    "INSTAGRAM",
    "MESSENGER",
    "THREADS"
  ],
  "firstShown": "2026-08-10",
  "lastShown": "2026-09-02",
  "headline": "Finde deinen Flow",
  "body": "Vom ersten Training bis zum Schlusspfiff: Entdecke Basketball-Essentials für einen starken Auftritt bei jeder Partie.",
  "destinationUrl": "https://fb.com/canvas_doc/1678531143227828",
  "primaryAssetUrl": "https://scontent-fra3-1.xx.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
  "sourceUrl": "https://www.facebook.com/ads/library/?id=28093431340342783",
  "assetUrls": [
    "https://scontent-fra3-1.xx.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
    "https://video-fra3-1.xx.fbcdn.net/o1/v/t2/f2/m366/AQPSADM-Enxo3uSlcLMidGPDSWBtXcDQ1m2ppKuyoQj2i17yj7QKhxvkoRjy5rsJjs8sVtLCxTyNUCDFou2m4q4F5Lbcijd7waYLnBYikH8cOg.mp4?...",
    "https://video-fra3-1.xx.fbcdn.net/o1/v/t2/f2/m412/AQN0CU95pKgN-QCGEacKjDWf4SpH6xBIY6AFnqONJywgMOJli2GRNoM3qb6ytN0NRVNyS45l-uJsoL1XzoIx1iVuys_3KRLaHfvmcIOKpQ.mp4?..."
  ],
  "previous": {
    "advertiserId": "15087023444",
    "advertiserName": "Nike",
    "creativeId": "28093431340342783",
    "format": "VIDEO",
    "regions": [
      "DE"
    ],
    "firstShown": "2026-08-10",
    "lastShown": "2026-09-02",
    "headline": "Finde deinen Flow",
    "body": "Vom ersten Training bis zum Schlusspfiff: Entdecke Basketball-Essentials für einen starken Auftritt bei jeder Partie.",
    "destinationUrl": "https://fb.com/canvas_doc/1678531143227828",
    "platforms": [
      "AUDIENCE_NETWORK",
      "FACEBOOK",
      "INSTAGRAM",
      "MESSENGER",
      "THREADS"
    ],
    "ctaText": "Shop now",
    "ctaType": "SHOP_NOW",
    "collationCount": 3,
    "assetUrls": [
      "https://scontent-fra3-1.xx.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
      "https://video-fra3-1.xx.fbcdn.net/o1/v/t2/f2/m366/AQPSADM-Enxo3uSlcLMidGPDSWBtXcDQ1m2ppKuyoQj2i17yj7QKhxvkoRjy5rsJjs8sVtLCxTyNUCDFou2m4q4F5Lbcijd7waYLnBYikH8cOg.mp4?...",
      "https://video-fra3-1.xx.fbcdn.net/o1/v/t2/f2/m412/AQN0CU95pKgN-QCGEacKjDWf4SpH6xBIY6AFnqONJywgMOJli2GRNoM3qb6ytN0NRVNyS45l-uJsoL1XzoIx1iVuys_3KRLaHfvmcIOKpQ.mp4?..."
    ],
    "primaryAssetUrl": "https://scontent-fra3-1.xx.fbcdn.net/v/t39.35426-6/771708732_1334021901815736_3767698710093745293_n.jpg?...",
    "sourceUrl": "https://www.facebook.com/ads/library/?id=28093431340342783",
    "assetHash": "b6cd5862b7ebe46d03e30b19d826f2228c48a6c2694d83b3b2c0bdab16e1430b"
  },
  "current": null,
  "evidence": {
    "consecutiveCompleteAbsences": 2,
    "firstMissingAt": "2026-09-03T04:00:00.000Z",
    "confirmationIntervalMs": 25200000,
    "runId": "competitor-watch:15087023444:DE:active:2026-09-03T11:00:00.000Z",
    "sourceEvidence": {
      "completenessReason": "PAGINATION_EXHAUSTED",
      "nextPageTokenPresent": false,
      "pageCount": 6,
      "approximateSourceCount": 60,
      "adStatus": "active",
      "transport": "replay"
    }
  },
  "detectedAt": "2026-09-03T11:00:00.000Z",
  "schemaVersion": "1.0"
}
```

</details>

### Input fields

| Field | Description | Default |
|---|---|---|
| `pageIds` | Up to 20 Facebook Page IDs, Page URLs, Page usernames, or Ad Library URLs | Empty |
| `keywords` | Up to 10 search terms, each monitored as a new-launch feed | Empty |
| `keywordSearchType` | `keyword_unordered` (broad match) or `keyword_exact_phrase` | `keyword_unordered` |
| `country` | Delivery country, or `ALL` for the complete active library | `ALL` |
| `monitorName` | Stable history name for one watchlist | `competitor-watch` |
| `mode` | `AUTO`, `FULL_LIFECYCLE`, or `NEW_LAUNCH_FEED` | `AUTO` |
| `maxAdsPerPage` | Per-Page and per-keyword result and billing safety cap, up to 5,000 | `300` |
| `outputMode` | Current ads plus changes, or changes only | `ALL_CHECKED` |
| `includeBaseline` | In changes-only mode, label first-run ads as `NEW` | `false` |
| `includeTentative` | Include internal `MISSING_ONCE` events | `false` |
| `proxyConfiguration` | Apify Proxy settings; residential proxies are the default and included in the price | Apify residential |

### Monitoring modes

#### Auto

Recommended for most users. Full lifecycle is used only when all result pages finish. Large advertisers automatically fall back to a safe new-launch feed.

#### Full lifecycle

Requires a complete scan of the Page's active ads. If the configured cap is reached, the target is reported as `INCOMPLETE`, history is not advanced, and no ad scans are billed for that target.

#### New launch feed

Tracks previously seen Ad Library IDs and emits only new ones. This mode can operate on a capped snapshot because it never produces stopped-ad claims. Keyword targets always use this mode.

### Keyword targets

Add search terms to `keywords` when you do not know the Page yet or want to watch a whole product category:

```json
{
  "keywords": ["running shoes", "protein powder"],
  "keywordSearchType": "keyword_unordered",
  "country": "US",
  "maxAdsPerPage": 300,
  "outputMode": "CHANGES_ONLY"
}
```

Each keyword receives its own history scope (`monitorName`, keyword, match type, country). Every row carries the advertiser's real Page ID and name in `advertiserId` and `advertiserName` and the search term in `keyword`. The first run returns the current ranked ads; later runs emit `NEW` or `DISCOVERED` rows for ads that were not seen before. A missing ad is never reported as stopped, because it may simply have dropped out of Meta's ranking. When you find a Page worth monitoring closely, add its Page ID to `pageIds` to get full stopped-ad detection.

### Output modes

#### Changes only

Returns only lifecycle events. Use this for schedules, webhooks, Slack, Discord, Make, Zapier, and n8n. The first baseline produces zero rows because existing ads are not new launches.

#### Changes and all checked ads

The default mode. Adds one `CURRENT` row per checked ad and includes any verified change events. Use it for the first run, one-time exports, audits, and swipe-file workflows.

### Ad fields by format

The Ad Library exposes different structured fields for each ad format. A `null` value means the source did not provide that field. The Actor does not guess missing copy, OCR text embedded in artwork, or transcribe videos.

| Format | Consistently available | Notes |
|---|---|---|
| `IMAGE` | Primary text, headline, destination URL, call-to-action, original and resized image URLs | Text embedded in an image is not OCR output. |
| `VIDEO` | Primary text, headline, destination URL, call-to-action, HD and SD video URLs, preview image | Spoken and on-screen text are not transcribed. |
| `CAROUSEL` | Card titles, card bodies, card links, and card media | The first card supplies `headline`, `body`, and `destinationUrl` when the top-level snapshot is empty. `cardCount` is inside `current`. |
| `DCO`, `DPA`, and `MULTI_IMAGES` | Every image, video, and text variant Meta returns | Dynamic and catalog ads can rotate variants; asset identity uses the full media set. |

Media URLs are signed by Meta and expire after a few weeks. Download assets you want to keep soon after the run. Asset identity never depends on the signed parts, so expiry does not create change events.

### Supported countries

`ALL` returns every active ad of the Page regardless of delivery country. The single-country filter supports 63 markets across Europe, the Americas, Asia-Pacific, the Middle East, and Africa, including `US`, `GB`, `CA`, `AU`, `DE`, `FR`, `ES`, `IT`, `NL`, `BR`, `MX`, `IN`, `JP`, `AE`, `ZA`, and `TR`.

### How to find the Page ID

1. Open the Meta Ad Library.
2. Search for the advertiser and open its ads.
3. Copy the URL. It contains `view_all_page_id=<number>`.

Example:

```text
https://www.facebook.com/ads/library/?active_status=active&ad_type=all&country=ALL&view_all_page_id=15087023444&search_type=page
```

You can paste the full Ad Library URL, the numeric Page ID, the Page URL (`https://www.facebook.com/nike`), or the username (`nike`). Usernames must match the Page's exact username; when no exact match exists the Actor reports `PAGE_ALIAS_NOT_FOUND` instead of guessing a similar Page.

### Scheduling and webhooks

For daily monitoring:

1. Save the Actor input as an Apify Task.
2. Keep the same `monitorName` and scope settings.
3. Add an Apify Schedule.
4. Add a webhook for `ACTOR.RUN.SUCCEEDED`.
5. Read the default dataset. Zero rows means no confirmed change was emitted.

Every change includes a deterministic `eventId`. Use it as the idempotency key when a webhook or downstream workflow retries delivery.

Changing `monitorName`, the Page ID, the keyword, the keyword match type, or `country` creates a different history scope.

This Actor is also exposed to AI agents through Apify's MCP server (mcp.apify.com): an agent can discover it by search and run it with the same pay-per-event billing, with no separate integration.

### Billing

The Actor separates the cost of scanning from the cost of confirmed changes. An unchanged ad can still incur the scan charge when it is hidden by `CHANGES_ONLY`, because the source request, Page isolation, and history comparison were completed.

- Ads scanned: **$1.50 per 1,000**
- Verified changes: **$20.00 per 1,000**
- Actor start: **$0.005 per GB of run memory**
- Platform usage and residential proxies: included

The first accepted observation creates a baseline and never incurs verified change charges. Later runs charge that event only for confirmed new, stopped, resumed, or modified ads.

Example scan costs before any later verified changes:

| Ads scanned | Scan cost |
|---:|---:|
| 10 | $0.015 |
| 100 | $0.15 |
| 1,000 | $1.50 |

One Page with 100 active ads checked daily for 30 days costs $4.50 in scan charges, plus the Actor start for each run and any verified change events.

The default 300-ad cap limits scan charges to $0.45 per Page. With 20 Pages at the full default cap, the maximum scan charge is $9.00 plus verified changes and the Actor start. Raise the cap up to 5,000 for very large advertisers; most advertisers run far fewer active ads.

Dataset rows are not a separate billable event, so `ALL_CHECKED` does not create a second scan charge.

These cases are not billed as ad scans:

- Page lookup failure, unknown Page ID, or invalid keyword
- Source request failure or block
- Page contamination (an ad from a different Page)
- Explicit `FULL_LIFECYCLE` run that cannot finish pagination
- Suspicious empty snapshot after a previously active library
- Records beyond the run's maximum charge limit

The `OUTPUT` record contains exact scan and change billing counters plus a per-Page accounting summary.

### Important limitations

- The Meta Ad Library is a public transparency source, not the Marketing API. The Actor does not provide spend, reach, impressions, conversions, or campaign performance for regular commercial ads.
- The Actor monitors the ads Meta reports as active. Inactive ads are not part of the snapshot, so an ad that stops is detected through its absence, not through an end date.
- A missing Ad Library record does not prove that a campaign was deleted inside Ads Manager.
- A complete but empty snapshot cannot erase a previously active library. The target is flagged as `SUSPICIOUS_EMPTY`, history is preserved, and no stopped events are emitted.
- Media links depend on what Meta exposes publicly and expire after a few weeks.
- Very large advertisers may use `NEW_LAUNCH_FEED` in Auto mode rather than full stopped-ad detection.
- The Actor monitors one delivery country (or `ALL`) per run. Create separate tasks for separate countries.
- Keyword targets follow Meta's ranking. They are a discovery and new-launch feed, not a complete advertiser library, so they never confirm stopped ads and their `DISCOVERED` rows can include older ads that entered the ranking.
- Avoid overlapping runs that use the same monitor name and scope. Schedule the next run after the previous run finishes.
- Meta reports dates at day-level granularity. A `NEW` event means the ad's start date is on or after the previous accepted scan date, not that the exact launch minute is known.
- Meta rate-limits datacenter IP addresses, so the Actor routes its single browser session through Apify residential proxies by default. Proxy usage is included in the event price. A blocked or rate-limited session is retried once and then reported as a failed target without any ad scan charge.

### FAQ

#### Can I search by keyword such as "running shoes"?

Yes. Add the term to `keywords`. Keyword targets return the ranked active ads for that term with the real Page behind every ad and track new launches across runs. Stopped-ad detection stays reserved for exact Page targets, because keyword rankings change between runs.

#### Why was my first dataset empty?

You selected `CHANGES_ONLY`, so the first successful run created a silent baseline. Use the default `ALL_CHECKED` mode when you want the current ad snapshot on the first run.

#### Why did Auto mode use New Launch Feed?

The Page had more active ads than the configured cap. Auto mode refused to make stopped-ad claims from a partial snapshot.

#### Does a stopped event require two days?

It requires two consecutive complete scheduled runs, not a fixed number of days. With a daily schedule, confirmation normally takes two daily absences.

#### Can I monitor several competitors?

Yes. Add up to 20 Pages. Every Page receives an independent state and completeness decision.

#### Is this affiliated with Meta?

No. This Actor is not affiliated with or endorsed by Meta Platforms. Users are responsible for using public data lawfully and in accordance with applicable rules.

# Actor input Schema

## `pageIds` (type: `array`):

Paste up to 20 Page targets. Accepted: a numeric Page ID (15087023444), an Ad Library URL that contains view\_all\_page\_id, a Page URL such as https://www.facebook.com/nike, or a Page username. Usernames are resolved to the exact Page ID before monitoring, so unrelated advertisers are never mixed into a Page monitor. Leave empty when you only use keywords.

## `keywords` (type: `array`):

Optional search terms such as a product, brand, or competitor name, up to 10. Each keyword becomes a new-launch feed of the active ads Meta ranks for that term in the selected country. Keyword results mix many advertisers and change ranking over time, so stopped-ad detection is never claimed for keyword targets.

## `keywordSearchType` (type: `string`):

Broad match returns ads that contain all keyword words in any order. Exact phrase returns ads that contain the keyword as one phrase.

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

Return ads that Meta reports as delivered in this country. All countries returns the complete active library of the Page. Monitor the same Page separately when you need several single-country views.

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

Stable name used to compare scheduled runs; keep it unchanged for one watchlist. The first observation of a monitor bills zero verified changes, so starting a new watchlist never charges for baseline events.

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

Auto uses full lifecycle when every page of results finishes and safely falls back to new-launch tracking for very large advertisers.

## `maxAdsPerPage` (type: `integer`):

Safety and scan-cost cap for each Page or keyword target. At the default cap, scanning costs at most $0.45 per Page or keyword plus verified changes and the Actor start. Raise it up to 5,000 for very large advertisers; Auto mode switches to new-launch tracking when a complete active library exceeds this limit.

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

Current ads plus changes is best for the first run and one-time exports. Changes only is best for scheduled alerts.

## `includeBaseline` (type: `boolean`):

Advanced option for changes-only output. When enabled, the first successful run labels existing ads as NEW instead of creating a silent baseline. Current-ads output already returns them as CURRENT.

## `includeTentative` (type: `boolean`):

Expose MISSING\_ONCE rows before a second complete absence confirms that an ad stopped.

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

Meta rate-limits datacenter IP addresses, so the Actor uses Apify residential proxies by default. Proxy usage is included in the event price; keep the default unless you route traffic through your own proxies.

## `failOnAllTargetsFailed` (type: `boolean`):

Fail the run after writing diagnostics when every Page target fails.

## Actor input object example

```json
{
  "pageIds": [
    "15087023444"
  ],
  "keywordSearchType": "keyword_unordered",
  "country": "ALL",
  "monitorName": "competitor-watch",
  "mode": "AUTO",
  "maxAdsPerPage": 300,
  "outputMode": "ALL_CHECKED",
  "includeBaseline": false,
  "includeTentative": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "failOnAllTargetsFailed": true
}
```

# Actor output Schema

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

Default dataset with current ads plus new, discovered, stopped, resumed, and changed rows.

## `runSummary` (type: `string`):

OUTPUT record with per-Page completeness, mode selection, event counts, billing, and diagnostics.

# 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 = {
    "pageIds": [
        "15087023444"
    ],
    "maxAdsPerPage": 300,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("kamerozkan/facebook-ad-library-change-monitor").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 = {
    "pageIds": ["15087023444"],
    "maxAdsPerPage": 300,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("kamerozkan/facebook-ad-library-change-monitor").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 '{
  "pageIds": [
    "15087023444"
  ],
  "maxAdsPerPage": 300,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call kamerozkan/facebook-ad-library-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kamerozkan/facebook-ad-library-change-monitor"
        }
    }
}

```

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/ZFjVfFgEdFJomArAb/builds/NzPUsUw4NQ3DDnldx/openapi.json
