# Meta Ad Library Monitor: New Competitor Ads (`singletonops/meta-ad-library-new-ads-monitor`) Actor

Monitor the public Meta Ad Library for the advertisers you choose and get only the Facebook and Instagram ads that are new, changed or ended since your last run, with copy, CTA, landing page, media links and an Ad Library link. Schedule it for competitor ad-launch alerts. No login, no API token.

- **URL**: https://apify.com/singletonops/meta-ad-library-new-ads-monitor.md
- **Developed by:** [SingletonOps](https://apify.com/singletonops) (community)
- **Categories:** Marketing, Social media, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 25.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Meta Ad Library Monitor: New Competitor Ads

**Monitor competitors' Facebook and Instagram ads** in the public Meta Ad Library and get only the ads that are new, changed or ended since your last run, with copy, CTA, landing page, media links and an Ad Library link. Schedule it for competitor ad-launch alerts. No login, no API token. Built by SingletonOps (a brand of Vibex LLC).

Covers every Meta surface in the Ad Library (Facebook, Instagram, Messenger, Audience Network, Threads). A `changed` ad means its copy, title, CTA, link, media or platforms moved; an `ended` ad stopped running. Every run also writes a summary, and an optional webhook pushes the changes to you. The actor only reads what anyone can see at [facebook.com/ads/library](https://www.facebook.com/ads/library/).

### What it does

1. For every advertiser it opens the public Ad Library search (by page or keyword) with your country, active-status, platform and media filters.
2. It pages through all results (30 per request) up to `maxAdsPerAdvertiser`.
3. It compares the ads with the state saved in a **named key-value store** from the previous run (the same stateful diffing used by our RSS and Sitemap monitors).
4. It outputs **only** `new`, `changed` and `ended` ads, plus one `summary` record, and optionally POSTs them to your webhook.

### Use cases for PPC teams

- **Know the day a competitor launches.** Schedule every 6 hours or daily with a fixed `stateStoreName`; each run outputs only `new`, `changed` and `ended` ads.
- **Paid-social creative reviews.** Feed new ads (copy, CTA, media links) into a swipe file or a Google Sheet for the weekly creative meeting.
- **Spot offer changes fast.** `changed` ads show edits to copy, title, CTA, link or media, often a new promotion or price test.
- **See what they killed.** `ended` ads tell you which tests a competitor stopped, which is as useful as what they launched.
- **Agency client monitoring.** One task per client and country; send the webhook to Slack, Zapier or Make so account managers see changes without logging in.
- **Find proven winners.** Turn on `longRunningOnly` (or set `minDaysRunning`, e.g. 90) to keep only ads that have been running for 6+ months: the creatives a competitor keeps paying for. Every row has `firstSeenDate`, `lastSeenDate` and `daysRunning`, so you can also just sort by `daysRunning`.
- **See when a promotion really ended.** `lastSeenDate` is the ad's end date once Meta stops delivering it, so you know when a sale ad actually stopped, whatever the landing page still says.
- **Keyword watch.** Use `keyword:` advertisers to watch everyone advertising on a topic, not just known competitors.

### Setup: how to monitor competitor Meta ads

1. Open the actor and add your competitors to **Advertisers** (page ID, Facebook page URL, Ad Library URL or `keyword:` search).
   **Use the page ID to monitor a brand.** A `keyword:` search matches *any* advertiser whose ads mention the word, not just that brand. In a test run, `keyword:allbirds` (all statuses, US) returned 50 ads but only 6 came from Allbirds' own page (`778794852137593`); the rest came from other advertisers mentioning "allbirds". Find a brand's page ID in its Ad Library URL (`view_all_page_id=...`), or paste its Facebook page URL. Keep `keyword:` for topic watches. When a keyword target is used, the run log, the status message and the `summary` row (`hints`, `matchedPages`) suggest the page IDs it matched.
2. Pick a **country** and keep `activeStatus: active`.
3. Set a fixed **State store name** for this monitor (for example `competitors-us`).
4. Click **Start**. The first run saves a baseline (every current ad is output once as `new` with `isBaseline: true`, unless you turn that off).
5. Save the input as a task and add a **Schedule** (for example every 6 hours or daily). Every later run outputs only new, changed and ended ads.
6. Optional: add a `webhookUrl`, or connect Slack, Google Sheets, Zapier or Make through Apify integrations.

### Input

| Field | Default | Notes |
| --- | --- | --- |
| `advertisers` | – (required) | Page IDs (`15087023444`), page URLs (`https://www.facebook.com/Gymshark`), Ad Library URLs (`...?view_all_page_id=...` or `...?q=...`), or keywords. `keyword:` forces a keyword search. Max 100. |
| `country` | `US` | 2-letter ISO code or `ALL`. |
| `activeStatus` | `active` | `active`, `inactive` or `all`. |
| `platforms` | all | `facebook`, `instagram`, `messenger`, `audience_network`, `threads`. |
| `mediaType` | `all` | `all`, `image`, `video`, `meme`, `image_and_meme`, `none`. |
| `exactPhrase` | `false` | Keyword targets only: search the exact phrase (Meta's `keyword_exact_phrase`, the query is sent in double quotes like the Ad Library UI) instead of any words. Page IDs and page URLs ignore it. On a keyword it starts its own baseline. Note: for a pasted Ad Library URL only `q=` is read; its `search_type` is not, so set this option for exact-phrase URLs. |
| `maxAdsPerAdvertiser` | 500 | Safety cap (1–5000). Counts every ad checked, including ads the long-running filter drops. |
| `longRunningOnly` | `false` | Long-running ads only: keep ads with `daysRunning` ≥ 180 (about 6 months). |
| `minDaysRunning` | `0` (off) | Keep ads with `daysRunning` ≥ this many days (0–3650). With `longRunningOnly` too, the larger value wins. |
| `stateStoreName` | `meta-ad-library-monitor-state` | Use one store per monitor/schedule. |
| `emitAllOnFirstRun` | `true` | First run outputs every current ad as `new` with `isBaseline: true`. Set `false` for a silent baseline. |
| `emitEnded` | `true` | Output ads that stopped running. |
| `webhookUrl` | – | Receives `{ summary, items (≤200), truncated, datasetId }` when something changed or an advertiser failed. |
| `proxyConfiguration` | Apify RESIDENTIAL | Required in practice (see *Anti-bot notes*). |
| `maxRetries`, `requestTimeoutSecs`, `pruneEndedAfterDays` | 4, 45, 90 | Advanced. |

Example:

```json
{
  "advertisers": ["15087023444", "https://www.facebook.com/Gymshark", "keyword:allbirds"],
  "country": "US",
  "activeStatus": "active",
  "maxAdsPerAdvertiser": 500,
  "stateStoreName": "competitors-us",
  "webhookUrl": "https://hooks.example.com/meta-ads"
}
```

**Long-running ads only (6+ months)**

```json
{
  "advertisers": ["15087023444"],
  "country": "US",
  "longRunningOnly": true,
  "stateStoreName": "nike-long-running"
}
```

The filter runs after `daysRunning` is computed. Ads below the bar (or without a Meta start date) are not output, not charged and not tracked; one that later crosses the bar shows up as `new` that run. Turning the filter on starts its own baseline for the same `stateStoreName`, so your existing monitor's state isn't affected. Ads the filtered monitor already tracks are still followed, so their changes and their end are reported.

### Output fields

| Field | Description |
| --- | --- |
| `changeType`, `changedFields`, `previous` | `new`, `changed` or `ended`; for `changed`, which fields moved and their old values |
| `adArchiveId`, `adLibraryUrl` | Meta's ad ID and a direct link to the ad in the Ad Library |
| `pageId`, `pageName`, `pageUrl` | The advertiser's Facebook page |
| `isActive`, `startDate`, `endDate` | Whether the ad is running and its day-level run dates |
| `firstSeenDate`, `lastSeenDate`, `daysRunning` | Ad longevity. `firstSeenDate` = delivery start (`YYYY-MM-DD`). `lastSeenDate` = the run date (UTC) while the ad is active, or its end date once it stopped. `daysRunning` = days from first to last, counting both days (same day = 1). `null` when Meta gives no date; never guessed |
| `endDateStale`, `dataNote` | `true` + a short note when Meta says the ad is active but its `endDate` is already past (stale Meta data, not a parsing issue); otherwise `false` / `null` |
| `platforms` | `facebook`, `instagram`, `messenger`, `audience_network`, `threads` |
| `bodyText`, `title`, `caption`, `linkDescription` | Ad copy |
| `ctaText`, `ctaType`, `linkUrl` | Call to action and landing page |
| `displayFormat`, `imageUrls`, `videoUrls`, `videoPreviewUrls`, `cards`, `cardsCount` | Creative format and media (carousel / catalog cards included) |
| `collationId`, `collationCount`, `categories` | Meta's grouping of near-identical ads, and ad categories |
| `firstSeen`, `lastSeen`, `detectedAt`, `isBaseline` | When this monitor first and last saw the ad (timestamps, not the ad's own run dates; see `firstSeenDate` / `lastSeenDate`); `isBaseline` marks first-run rows |
| `advertiserInput`, `country` | Which input and country produced the row |

The `summary` row also has, per advertiser:

| Field | Description |
| --- | --- |
| `status`, `complete`, `error`, `errorCategory` | `OK` / `PARTIAL` / `FAILED`; `complete: true` means the list was scanned to the end, so ended ads were detected |
| `totalReported`, `adsSeen` | The result count Meta reported, and the ads actually returned |
| `reportedGap` | Informational only: `totalReported - adsSeen` when a **complete** scan saw fewer ads than Meta reported (Meta's count includes ads it doesn't show to logged-out visitors and collated copies). It does **not** make the scan `PARTIAL`, so ended-ad detection still runs. `null` otherwise |
| `hint`, `matchedPages` | Keyword targets only: a reminder to use a page ID for brand monitoring, and the top 5 pages (`pageId`, `pageName`, `ads`) the keyword matched. The run-level `hints` array collects them |

### Output example

One dataset item per new / changed / ended ad:

```json
{
  "type": "ad",
  "changeType": "changed",
  "changedFields": ["bodyText"],
  "previous": { "bodyText": "Old copy" },
  "adArchiveId": "1107386451952079",
  "pageId": "15087023444",
  "pageName": "Nike",
  "pageUrl": "https://www.facebook.com/nike/",
  "isActive": true,
  "startDate": "2026-09-01T07:00:00.000Z",
  "endDate": "2026-10-12T07:00:00.000Z",
  "firstSeenDate": "2026-09-01",
  "lastSeenDate": "2026-10-12",
  "daysRunning": 42,
  "endDateStale": false,
  "dataNote": null,
  "platforms": ["facebook", "instagram"],
  "bodyText": "Run through summer with gear that can take the heat.",
  "title": "Nike Air Monarch IV",
  "caption": "NIKE.COM",
  "linkDescription": null,
  "ctaText": "Shop now",
  "ctaType": "SHOP_NOW",
  "linkUrl": "https://www.nike.com/",
  "displayFormat": "DPA",
  "imageUrls": ["https://scontent...fbcdn.net/..."],
  "videoUrls": [],
  "videoPreviewUrls": [],
  "cardsCount": 6,
  "cards": [{ "title": "Nike Air Monarch IV", "body": "...", "linkUrl": "...", "ctaText": "Shop Now", "imageUrl": "...", "videoUrl": null }],
  "collationCount": null,
  "collationId": null,
  "categories": [],
  "adLibraryUrl": "https://www.facebook.com/ads/library/?id=1107386451952079",
  "firstSeen": "2026-10-12T14:59:16.000Z",
  "lastSeen": "2026-10-12T15:04:31.000Z",
  "isBaseline": false,
  "advertiserInput": "15087023444",
  "country": "US",
  "detectedAt": "2026-10-12T15:04:31.000Z"
}
```

`changeType`:

- **new**: first time this ad is seen for this search. On the first run these have `isBaseline: true`.
- **changed**: body, title, CTA, landing link (tracking params ignored), media set (fbcdn signatures ignored) or platforms changed, or an ended ad came back. `changedFields` and `previous` show what changed.
- **ended**: Meta now shows the ad as inactive, or (with `activeStatus: active`) it disappeared from a **fully scanned** list. For ads that vanished, the text fields come from the last known state and media URLs are empty, and `firstSeenDate` / `daysRunning` are `null` (the state doesn't keep the start date).

The last dataset item is a `summary` record (also saved as `OUTPUT` in the run's key-value store) with per-advertiser status (`OK` / `PARTIAL` / `FAILED`), counts, whether the scan was complete, and HTTP/anti-bot stats. A `QUALITY_REPORT` record has field completeness and speed.

### Creative velocity (optional)

Turn on **Add creative velocity to the summary** (`includeCreativeVelocity: true`) to see how fast each advertiser ships new creative. It is off by default, adds no requests and no charges, and rides on the free summary row (plus a `CREATIVE_VELOCITY` key-value record). Per advertiser:

- `newAds7d`: ads whose Meta start date is in the last 7 days
- `last7d.formatMix`: static / video / carousel counts and percentages
- `last7d.uniqueLandingPages` + `topLandingPages`: landing pages by domain + path, without query strings
- `last7d.copyVariants`: distinct headline + primary-text variants
- `score` (0-100) and `band`: volume 50 pts (20+ new ads a week = full), format mix 15, landing pages 20 (5+ = full), copy variants 15 (10+ = full). Band: 20+ new ads = top, 10-19 active, 3-9 slow, 0-2 stalled
- `finding`: one line, e.g. "Gymshark launched 28 new ads in the last 7 days, 42.9% static, across 8 landing pages."

Counts are a floor when **Max ads per advertiser** stops the scan early (`sampleComplete: false`).

### How the diff stays reliable

- **Partial runs never invent "ended" ads.** Disappearance counts as ended only when the advertiser's list was scanned to the end without errors. Pagination failures mark the advertiser `PARTIAL` and keep everything collected so far.
- **No noise from rotating data.** Signed media URLs, tracking parameters, the daily-moving `endDate` of running ads, and the card order of catalog (DPA) and dynamic-creative (DCO) ads are excluded from the fingerprint.
- **"Reported but empty" is never a complete scan.** If Meta reports a result count above 0 but returns no ads (in the page and in the GraphQL first page), the advertiser is marked `PARTIAL` with `errorCategory: REPORTED_BUT_EMPTY`, `complete: false`: nothing is charged and no tracked ad is marked ended. If a request had already failed (e.g. `RATE_LIMITED`), that category is kept. Entries that could not be parsed show as `PARSE`.
- **State per search.** The state key includes the advertiser, country, status, media type and platforms, so changing a filter starts a fresh baseline instead of a flood of false changes.
- **Crash-safe ordering.** Changes are pushed before state is saved, so a crash re-emits rather than drops changes.
- Ended ads are kept for `pruneEndedAfterDays` (so a re-launch shows up as `changed`), then forgotten.

### How much does it cost?

Pay-per-event, so a quiet run costs almost nothing:

| Event | Price | When it's charged |
| --- | --- | --- |
| Run start (`apify-actor-start`) | **$0.01 per run** | Once per run |
| `new-or-changed-ad` | **$0.005 per new or changed ad** | Each `new` or `changed` ad saved to the dataset |
| Ended ads, unchanged ads, `summary` row | Free | Never charged |

The Pricing tab on the Store page is always the authoritative price.

Examples:

- Daily monitor, 10 competitors, 15 new or changed ads on a typical day: $0.01 + 15 × $0.005 = **$0.085 per run**, about $2.55 per month.
- A quiet day with no changes: **$0.01**.
- First run: with `emitAllOnFirstRun: true` (default) every current ad is output once as `new` with `isBaseline: true` and is charged as a new ad (500 current ads = $2.51). Set `emitAllOnFirstRun: false` for a silent baseline that outputs and charges no ads.

Set **Max cost per run** in the run options to cap your spend. When the cap is reached, the actor stops after the current advertiser and keeps everything saved so far.

For reference, platform usage (compute + residential proxy) in our test runs was roughly **$0.03–0.06 per 1,000 ads checked** at 256 MB.

### Anti-bot notes

- Logged-out traffic from datacenter IPs is redirected to the Facebook login page. Residential proxy works.
- Each new session gets a 403 interstitial that POSTs to `/__rd_verify_…`; the actor solves it over plain HTTP (no browser needed).
- The first GraphQL pagination request in a new session often returns `1675004 Rate limit exceeded`. The actor backs off and retries on a new residential IP, re-reading session tokens and resuming from the saved cursor.
- The pagination `doc_id` is read from Meta's live JS bundle (cached for 12 h) with a built-in fallback.

### Limits

- `startDate` / `endDate` are day-level dates from the Ad Library. For running ads, `endDate` is "today" and moves daily (and is sometimes stale), so `lastSeenDate` uses the run date for active ads instead. When Meta lists an ad as active but its `endDate` is more than 2 days in the past, the row gets `endDateStale: true` and a `dataNote`; `isActive` stays as Meta reports it.
- Meta's dates are Pacific-time days (they arrive as 07:00 or 08:00 UTC), so `firstSeenDate` matches the date shown in the Ad Library. `lastSeenDate` for active ads is the UTC run date, which can be one day ahead of the Pacific date for evening runs.
- Meta groups near-identical ads ("collation"). The actor outputs the representative ad, so a keyword search can report fewer ads than the count Meta shows. Meta's count can also include ads it doesn't serve to logged-out visitors (for example, ads still in review), so a complete scan can return fewer ads than reported. That difference is shown as `reportedGap` and is not treated as an error.
- Keyword targets match any advertiser that mentions the word. Use page IDs to monitor specific brands.
- With `maxAdsPerAdvertiser` hit, ended-by-disappearance is skipped for that advertiser (status still `OK`).
- Spend/reach figures are only published for political/issue ads and EU deliveries and are not output in this version.

### Alerts: Slack, email, Zapier, Make or your own endpoint

1. Save the input as a **task** with a fixed `stateStoreName` and add a **schedule** (every 6 hours or daily).
2. **Built-in webhook:** set `webhookUrl`. After each run that found something (or where an advertiser failed), the actor POSTs `{ actor, summary, items, truncated, datasetId }` with up to 200 new / changed / ended ads. Point it at a Slack incoming-webhook relay, Zapier or Make catch hook, or your own endpoint. Quiet runs send nothing, so there's no alert noise.
3. **Apify integrations:** on the task's Integrations tab, connect Slack, email, Google Sheets, Zapier, Make or n8n to receive the run's dataset (which contains only changes plus one `summary` row).
4. Watch the `summary` row: an advertiser with status `FAILED` or `PARTIAL` means that monitor needs attention (usually a temporary Meta rate limit; the next run picks up where it left off, and partial runs never invent `ended` ads).

### FAQ

**Do I need a Facebook account or API token?** No. The actor reads only the public Ad Library.

**How often does it check?** Only when it runs. Schedule it in Apify (every 6 hours or daily works well for competitor tracking) and keep the same `stateStoreName`.

**Why did the first run output every ad?** That is the baseline (`isBaseline: true`). Set `emitAllOnFirstRun: false` for a silent baseline.

**How is `daysRunning` counted?** `lastSeenDate` minus `firstSeenDate` plus 1, so both days count: an ad that started today has `daysRunning: 1`. For active ads it grows by one each day; for ended ads it's fixed. If Meta gives no start date (or an inactive ad has no end date), it's `null`, and the long-running filter leaves that ad out. Our Google Ads Transparency scraper uses the same three fields and the same definition, so you can combine both datasets.

**Does it show ad spend or reach?** No. Meta publishes those only for political/issue ads and EU deliveries, and this version does not output them.

**Can I export to CSV or Google Sheets?** Yes. Every Apify dataset downloads as JSON, CSV, Excel or XML, and integrations push it to Google Sheets, Slack, Make, Zapier or n8n.

**Is it legal?** It collects only data Meta publishes publicly in the Ad Library. You are responsible for how you use it and for complying with Meta's terms and applicable law.

### Legal

This actor only reads ads Meta publishes in the public Ad Library, without logging in or bypassing any access control. It doesn't target personal data, but results can include advertiser and Page names. Make sure your use complies with Meta's Terms and the laws that apply to you (for example GDPR). If you're unsure, consult your lawyer. See Apify's post on [the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/).

### Support

Found a bug or need a field? Open an issue on the **Issues** tab.

Built and maintained by **SingletonOps**, a brand of Vibex LLC. Also from SingletonOps: Google Ads Transparency Scraper & Ad Monitor, RSS Change Monitor and Sitemap Change Monitor.

# Actor input Schema

## `advertisers` (type: `array`):

Facebook page IDs (e.g. 15087023444), facebook.com page URLs (e.g. https://www.facebook.com/nike), Ad Library URLs with view_all_page_id or q=, or search keywords. Prefix with "keyword:" to force a keyword search. To monitor a brand, use its page ID: a keyword matches any advertiser whose ads mention the word.

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

2-letter ISO country code the ads were delivered in, or ALL.

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

Which ads to watch. With "active" an ad that disappears from a fully scanned list is reported as ended.

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

Only ads shown on these platforms. Empty = all platforms.

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

Filter by creative media type.

## `exactPhrase` (type: `boolean`):

For keyword targets only: search the exact phrase (Meta's <code>keyword_exact_phrase</code>, the query is sent in double quotes like the Ad Library UI does) instead of the default any-words search. Page IDs and page URLs ignore it. Turning it on starts its own baseline for the same <code>stateStoreName</code>.

## `maxAdsPerAdvertiser` (type: `integer`):

Safety cap on ads checked per advertiser per run (30 ads per request). If the cap is hit, ended-ad detection by disappearance is skipped for that advertiser. The form starts at 30 (one page, fastest trial run); raise it for full ended-ad tracking. API runs that omit it use the default of 500.

## `longRunningOnly` (type: `boolean`):

Keep only ads that have been running for at least 180 days (<code>daysRunning</code> ≥ 180), the "proven winners" an advertiser keeps paying for. Same as <b>Minimum days running</b> = 180; if both are set, the larger value wins. Active ads count up to today; ended ads up to their end date. Ads without a Meta start date are left out while this is on. Filtered-out ads are not saved or charged; <b>Max ads per advertiser</b> still counts every ad checked.

## `minDaysRunning` (type: `integer`):

Keep only ads whose <code>daysRunning</code> (from the delivery start date to today for active ads, or to the end date for ended ads, counting both days) is at least this many days, e.g. 90 or 180. 0 = off (default). Turning the filter on starts its own monitoring baseline; ads already tracked by that baseline are still followed until they end.

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

Named key-value store that keeps what was seen between runs. Use one name per monitor / schedule. Changing the country or filters starts a fresh baseline automatically.

## `emitAllOnFirstRun` (type: `boolean`):

On the first run for an advertiser, output every current ad as "new" (isBaseline=true). Turn off to just record a silent baseline.

## `emitEnded` (type: `boolean`):

Output a record when an ad stops running.

## `includeCreativeVelocity` (type: `boolean`):

Optional. Adds a <code>creativeVelocity</code> block per advertiser to the free summary record (and a CREATIVE_VELOCITY key-value record): new ads started in the last 7 days, static / video / carousel mix, unique landing pages (domain + path), distinct headline + copy variants and a 0-100 velocity score where 20+ new ads a week is the top band. Uses only ads this run already checked, so it adds no requests and no charges. Counts are a floor when <b>Max ads per advertiser</b> stops the scan early.

## `webhookUrl` (type: `string`):

Optional. When there are changes, the run summary and up to 200 changed ads are POSTed here as JSON.

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

Meta sends logged-out datacenter traffic to a login wall, so residential proxy is the default and strongly recommended.

## `maxRetries` (type: `integer`):

Retries with exponential backoff and a fresh proxy session on rate limits / blocks.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout.

## `pruneEndedAfterDays` (type: `integer`):

Ended ads are kept in state for this long (so a reactivation is reported as a change), then dropped.

## Actor input object example

```json
{
  "advertisers": [
    "15087023444"
  ],
  "country": "US",
  "activeStatus": "active",
  "platforms": [],
  "mediaType": "all",
  "exactPhrase": false,
  "maxAdsPerAdvertiser": 30,
  "longRunningOnly": false,
  "minDaysRunning": 0,
  "stateStoreName": "meta-ad-library-monitor-state",
  "emitAllOnFirstRun": true,
  "emitEnded": true,
  "includeCreativeVelocity": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "maxRetries": 4,
  "requestTimeoutSecs": 45,
  "pruneEndedAfterDays": 90
}
```

# Actor output Schema

## `changes` (type: `string`):

No description

## `longRunning` (type: `string`):

No description

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

No description

## `quality` (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 = {
    "advertisers": [
        "15087023444"
    ],
    "maxAdsPerAdvertiser": 30,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("singletonops/meta-ad-library-new-ads-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 = {
    "advertisers": ["15087023444"],
    "maxAdsPerAdvertiser": 30,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("singletonops/meta-ad-library-new-ads-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 '{
  "advertisers": [
    "15087023444"
  ],
  "maxAdsPerAdvertiser": 30,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call singletonops/meta-ad-library-new-ads-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,singletonops/meta-ad-library-new-ads-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/rq7l7naeY4YxpPG2E/builds/GVcnV7SF43llJTcgV/openapi.json
