# Facebook & Meta Ads Library Scraper + Tracker (`luminar/meta-ads-library-creative-changefeed`) Actor

Scrape Facebook and Meta Ads Library creatives, copy, CTAs and landing links. Track competitor ads and detect new, updated and safely ended ads.

- **URL**: https://apify.com/luminar/meta-ads-library-creative-changefeed.md
- **Developed by:** [Luka](https://apify.com/luminar) (community)
- **Categories:** Social media, Marketing, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 baseline ad results

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

## Facebook & Meta Ads Library Scraper + Tracker

**Scrape Facebook and Meta Ads Library creatives, copy, CTAs and landing links — then detect new, updated and safely ended competitor ads on the next scheduled run.**

Point it at a brand, a Facebook page or a keyword. Get back the full ad copy, creatives, call-to-action, destination links and delivery dates from the [Meta Ad Library](https://www.facebook.com/ads/library). Run the same query again later and every ad comes back labelled — new, changed, unchanged, or gone.

***

### 🎯 What can this Actor do?

🔎 **Search by keyword, brand, page URL or Ad Library URL** — mix up to 25 targets in one run

📝 **Get the actual ad copy** — body text, headlines, descriptions, captions and CTA, including every card of a carousel

🖼️ **Get the creatives** — direct image and video URLs, without paying to download the media itself

🔗 **See where the ad sends people** — the destination link the advertiser attached to the ad

🌍 **Filter before you scrape** — country, active status, ad category, media type and date range, so you pay only for the ads you want

🔔 **Track changes over time** — every ad is labelled `NEW`, `UPDATED`, `UNCHANGED` or `ENDED` against your previous run, and an `ENDED` is only ever raised from a snapshot proven complete on both sides

✅ **Know when your data is complete** — every target reports whether the source was exhausted, capped or only partially read

***

### 🆚 Baseline or Changes — which one do I need?

This is the only real decision you have to make. Everything else has a sensible default.

| | **BASELINE** | **CHANGES** |
|---|---|---|
| What you get | Every matching ad in the current scan | The current scan with every row labelled `NEW`, `UPDATED`, `UNCHANGED` or safely proven `ENDED` |
| Answers | *"What is my competitor advertising?"* | *"What did my competitor change?"* |
| Best for | One-off research, creative swipe files, market scans | Daily or weekly monitoring, alerting, trend tracking |
| Needs a previous run | No | Second run onwards |
| Free-tier price | $0.02 run start + $0.70 / 1,000 delivered rows | $0.02 run start + $1.10 / 1,000 delivered monitoring rows |

Start with `BASELINE`. Switch to `CHANGES` when you want the Actor to do the comparing for you.

***

### ⬇️ Input

A one-off creative scan — a keyword and nothing else:

```json
{
  "searchTerms": ["running shoes"]
}
```

A competitor watch on two brands, tracking changes:

```json
{
  "productLevel": "CHANGES",
  "searchTerms": [
    "nike",
    "adidas"
  ],
  "country": "US",
  "maxAdsPerTarget": 100
}
```

Filtered market scan:

```json
{
  "searchTerms": ["protein powder", "creatine"],
  "country": "GB",
  "activeStatus": "ACTIVE",
  "mediaType": "VIDEO",
  "dateFrom": "2026-06-01",
  "maxAdsPerTarget": 100
}
```

You can paste a Meta Ad Library URL straight from your browser instead of using search terms — the Actor reads the filters out of the URL. Keyword searches and numeric-page-ID Ad Library URLs use the 256 MB default. An uncached Facebook handle such as `facebook.com/nike` may require the optional browser-continuity layer. That layer is deliberately disabled under the current three-event pricing and fails closed before browser work; use a numeric-page-ID Ad Library URL instead. Every target is capped at 100 rows; use multiple targets for larger batches.

***

### ⬆️ Output

One row per ad, per target. Real row from a live run, trimmed for length:

```json
{
  "adId": "388120997678322",
  "pageName": "adidas",
  "pageId": "1497383683837785",
  "pageCategories": ["Product/service"],
  "pageLikeCount": 43617936,
  "state": "INACTIVE",
  "isActive": false,
  "startDate": "2024-09-26T07:00:00.000Z",
  "endDate": "2026-05-20T07:00:00.000Z",
  "bodies": [
    "Conheça e compre todas as coleções e os principais lançamentos da adidas pelo app!"
  ],
  "cardTitles": [
    "Chuteira Mundial Team Unisex",
    "Chuteira Samba Classic Homem",
    "Tênis Samba Classic Homem"
  ],
  "ctaTexts": ["Shop now"],
  "ctaTypes": ["SHOP_NOW"],
  "observedLinkUrls": [
    "https://www.adidas.com.br/chuteira-mundial-team/019228.html"
  ],
  "imageUrls": ["https://scontent.xx.fbcdn.net/v/t39.35426-6/..."],
  "publisherPlatforms": ["FACEBOOK", "INSTAGRAM"],
  "displayFormat": "DPA",
  "coverageStatus": "CAPPED",
  "changeEventId": "7b31a4f7b7b0c5aee47c1ddedb4de8b1973f3ef95b6a82d17da1b5eaab0dcf19",
  "changeStatus": "NEW",
  "observedAt": "2026-08-20T12:03:44.512Z"
}
```

Export as JSON, CSV, Excel, XML or RSS, or pull it straight from the Apify API.

#### Built-in dataset views

- **`changes`** — the monitoring view: status, changed fields, advertiser, dates and direct Ad Library link.
- **`creativeResearch`** — a visual swipe-file view with creative preview, copy, CTA, format and landing links.
- **`allData`** — every normalized field for API and spreadsheet work.

#### What is in every row

| Group | Fields |
|---|---|
| **Ad identity** | `adId`, `collationId`, `collationCount`, `adArchiveAdCount` |
| **Advertiser** | `pageName`, `pageId`, `pageCategories`, `pageLikeCount`, `pageProfileUri`, `pageProfilePictureUrl` |
| **Copy** | `bodies`, `cardTitles`, `cardDescriptions`, `cardCaptions` |
| **Call to action** | `ctaTexts`, `ctaTypes` |
| **Creatives** | `imageUrls`, `videoUrls`, `displayFormat` |
| **Destination** | `observedLinkUrls` |
| **Delivery** | `state`, `isActive`, `startDate`, `endDate`, `publisherPlatforms` |
| **Disclosure** | `reachEstimate`, `spend`, `currency`, `impressionsText`, `categories`, `isPolitical` |
| **Change tracking** | `changeEventId`, `changeStatus`, `changedFields`, `materialFingerprint` |
| **Run context** | `country`, `query`, `queryKind`, `coverageStatus`, `observedAt` |

Meta publishes reach and spend for political and EU ads only. For ordinary commercial ads those fields stay `null` — the Actor never invents a number to fill a column.

***

### 💰 How much does it cost?

| Event | Price |
|---|---|
| Run start | $0.02 |
| Baseline result | $0.70 / 1,000 |
| Change result | $1.10 / 1,000 |

Real examples:

| What you run | You pay |
|---|---|
| 100 ads from one brand | ~$0.09 |
| 1,000 ads across 10 keywords | ~$0.72 |
| Daily Changes watch on 3 targets, 100 ads each | ~$0.35 per run |

The run-start event is charged once by the Actor after valid input. Baseline rows are charged only when accepted into your dataset. In Changes mode, every delivered `NEW`, `UPDATED`, `UNCHANGED` and safely proven `ENDED` row is paid because every repeat performs a fresh live scan; state reads/writes, summaries, failed rows and suppressed rows are not charged. A stable `changeEventId` distinguishes normal scan revisions while identifying a rare post-delivery replay. A delivery guard blocks automatic re-billing if the change state cannot be committed. You are not billed separately for compute, network use or storage. The current price does not include browser continuity; activating that bounded layer requires a separate fixed event and a separate pricing approval.

Run the same Changes query sequentially, never concurrently against the same change store. If a run reports `CHANGE_DELIVERY_RECOVERY_REQUIRED`, leave its guard intact and inspect the prior run's dataset and charged-event read-back before support clears it. This deliberate fail-closed state avoids guessing whether a prior delivery was billed.

The prices shown are the Free-tier rates. Apify subscription tiers receive the lower rates displayed on the Pricing tab.

***

### 📊 Measured, not estimated

Every figure below comes from private cloud runs through 2026-08-24 and was read back from the Apify API after billing settled. These are measured platform costs, not buyer prices; source conditions still vary.

| Test shape | Result | Cost per 1,000 |
|---|---|---:|
| One keyword, 5 ads at 256 MB (3 runs) | 5/5 `CAPPED` each | $0.234–$0.250 |
| Numeric-page-ID URL, 25 ads | 25/25 `CAPPED` | $0.065 |
| One keyword, 100 ads (3 runs) | 100/100 `CAPPED` each | $0.057–$0.105 |
| 10 keywords × 100 ads at 256 MB | 1,000 delivered, 937 globally unique | $0.030 |
| Changes, first snapshot | 60 `NEW` | $0.048 |
| Changes, repeat run | 60 `UNCHANGED`, zero false `UPDATED`/`ENDED` | $0.044 |
| Historical guarded-continuity release samples, 50 ads | 50/50 `CAPPED`; used to size the separate fixed event | $0.447–$0.491 |

Field coverage measured across the 1,000-row batch:

| Field | Filled |
|---|---:|
| Advertiser identity, state, start date | 100% |
| Creative asset URL | 99.3% |
| Ad body text | 97.2% |
| Call to action | 93.0% |
| Destination link | 91.8% |

Where Meta does not publish something, the field stays empty. No value is ever invented to fill a column.

### ✅ How do I know the data is complete?

Most scrapers hand you rows and let you guess whether that was everything. This one tells you, per target:

| Status | Meaning |
|---|---|
| `COMPLETE` | The source ran out of ads. You have all of them. |
| `CAPPED` | Your result limit was reached first. More ads exist. |
| `PARTIAL` | Rows are valid, but neither the cap nor the end was reached. |
| `EMPTY` | Proven zero matching ads — not a silent failure. |
| `CHALLENGED` | Meta blocked the read. Reported, never disguised as empty. |

This matters most for change tracking. **An incomplete run can never produce an `ENDED` event** — otherwise a slow network would look like your competitor switching off twelve campaigns. The Actor keeps the last known complete snapshot even after a partial run, so a temporary hiccup cannot corrupt tomorrow's comparison.

The trade-off is deliberate: this Actor would rather tell you an ad is still running than wrongly tell you it stopped. `COMPLETE` requires the source to be exhausted within the verified 100-row and 20-page per-target limits; increasing the number of targets does not turn a capped target into a complete one.

***

### ⚡ Automation ideas

Use your own private Apify Task or API call for a daily competitor watch, a weekly multi-target Changes report, or a one-off creative swipe file. Task creation, schedules and outbound alert workflows are separate setup steps; this release does not create or publish them.

**Need Google ads too?** Use the companion [Google Ads Transparency Scraper & Change Tracker](https://apify.com/luminar/google-ads-transparency-intelligence) — same output philosophy and the same change-tracking guarantees.

### 🤖 For AI agents / MCP

This public, limited-permission Actor can be discovered and run through the hosted [Apify MCP server](https://docs.apify.com/integrations/mcp). Use it when an agent needs current Meta competitor-ad creatives, source-backed ad fields, or a scheduled changefeed.

Example prompts:

- `Track what ads Nike is running on Facebook and Instagram in the US.`
- `Return only meaningful Meta ad changes for these competitor pages.`
- `Build a source-backed Meta creative swipe file with copy, CTA, media, and landing links.`

Connect the standard Apify MCP server; no Actor-specific MCP server is required.

***

### ❓ FAQ

**Is scraping the Meta Ad Library legal?**
The Ad Library is a public transparency tool that Meta publishes deliberately, and it shows the same data to anyone without logging in. That said, laws differ by country and by what you do with the data afterwards. You are responsible for your own use — if you plan something commercial or large-scale, get your own legal advice.

**Why not just use Meta's official API?**
Meta's `ads_archive` API only returns political and social-issue ads, plus housing, employment and credit ads in the US. Ordinary commercial ads have never been available through it outside the EU. For competitor research on normal brands, the Ad Library web data is the only source.

**How far back does the data go?**
Meta keeps commercial ads visible for about 90 days and political ads for seven years. That is a Meta limit, not an Actor limit — which is exactly why change tracking is worth starting early: your own history keeps growing after Meta drops theirs.

**Will I get duplicate rows?**
Deduplication happens inside each target. If the same ad matches two of your keywords, you get two rows — one per query context — because most buyers want to know which query found it. The run summary reports both delivered rows and globally unique ad IDs.

**Can I run this on a schedule?**
Yes. Use Apify Schedules and set `productLevel` to `CHANGES`. Each run returns the monitored snapshot with every delivered row labelled, so an automation can filter `NEW`, `UPDATED` and `ENDED` while still retaining `UNCHANGED` context. Combine it with webhooks or the Apify integrations for Slack, Zapier or Make to get alerts.

**What if Meta changes something?**
Every run reports coverage and route health. `AUTO_GUARDED` permits one bounded browser-continuity attempt only when its separate fixed PPE event is present at a safe runtime price. Under the current three-event pricing that event is absent, so the Actor fails closed before browser work instead of hiding an incomplete result or an unbounded cost. Choose `HTTP_ONLY` when you always want the lowest-cost route with no continuity attempt.

***

### 🔐 How your run is handled

- Live source access is restricted to the managed cloud runtime. Local execution fails closed before source access.
- Sensitive session and network details are not part of public rows or summaries.
- Media binaries are not downloaded — their URLs are already in the source data, so you get the links without paying for the bytes.
- The run summary records intended, charged, delivered, free and suppressed events, plus coverage and any controlled limit reached.

***

### 💬 Feedback

Found a field that should be there, a target type that fails, or a filter you need? Open an issue on the Actor's Issues tab. Bug reports with the run ID get fixed fastest.

# Actor input Schema

## `productLevel` (type: `string`):

<b>Baseline</b> returns the ads running right now. <b>Changes</b> returns the same data plus what is NEW, UPDATED, UNCHANGED or ENDED since your previous run of the same query. Start with Baseline.

## `searchTerms` (type: `array`):

Keywords to search across the Meta Ad Library. A run accepts up to 25 total targets across all input lists.

## `pageUrls` (type: `array`):

Track specific advertisers. Ad Library URLs containing a numeric page ID use the low-memory primary path and can be batched. An uncached handle such as <code>facebook.com/nike</code> may require optional browser continuity; that layer is disabled under the current three-event price and fails closed, so use a numeric-page-ID Ad Library URL instead.

## `maxAdsPerTarget` (type: `integer`):

Verified upper limit of ads collected for each search term or page. Use multiple targets for large batches; you are billed per delivered result.

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

Which country's ad delivery to read. Meta only offers <i>All countries</i> for political and issue ads — for ordinary commercial ads the run uses United States and reports it honestly in the output.

## `startUrls` (type: `array`):

Already built the search you want on <a href='https://www.facebook.com/ads/library' target='_blank'>facebook.com/ads/library</a>? Paste the URL here and the Actor reads your filters out of it.

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

Only ads currently running, only ads already stopped, or both.

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

Restrict to a single creative format.

## `adType` (type: `string`):

Political and issue ads unlock Meta's global all-country scope and its public reach and spend disclosures. Commercial ads do not carry those figures.

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

Leave empty for no lower bound.

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

Leave empty for no upper bound.

## `maxPaginationPages` (type: `integer`):

Advanced safety ceiling after the first response. The default is verified for the 100-result per-target limit. Setting it lower on purpose can report coverage as PARTIAL.

## `maxNetworkGb` (type: `number`):

Advanced hard network ceiling for the whole run. The Actor stops further collection and reports partial coverage instead of exceeding it.

## `browserFallbackPolicy` (type: `string`):

AUTO\_GUARDED permits one bounded browser-continuity step only when its separate fixed PPE event is active at a safe runtime price and the run has at least 2048 MB memory. Under the current three-event price the event is absent, so the Actor fails closed before browser work. HTTP\_ONLY always disables continuity.

## `timeoutSeconds` (type: `integer`):

Maximum wall-clock run time.

## `blockMediaDownloads` (type: `boolean`):

Creative URLs are read from the page data, so downloading the files themselves costs bandwidth and adds nothing. Leave on.

## Actor input object example

```json
{
  "productLevel": "BASELINE",
  "searchTerms": [
    "nike"
  ],
  "maxAdsPerTarget": 15,
  "country": "US",
  "activeStatus": "ALL",
  "mediaType": "ALL",
  "adType": "ALL",
  "maxPaginationPages": 20,
  "maxNetworkGb": 0.05,
  "browserFallbackPolicy": "AUTO_GUARDED",
  "timeoutSeconds": 900,
  "blockMediaDownloads": true
}
```

# Actor output Schema

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

Open the default dataset in the changes, creativeResearch or allData view.

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

Open the exact run status, target coverage, route health, intended/charged/delivered/free/suppressed PPE counts and provisional cost read-back.

## `runLedger` (type: `string`):

Open the machine-readable run event ledger.

# 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 = {
    "searchTerms": [
        "nike"
    ],
    "maxAdsPerTarget": 15
};

// Run the Actor and wait for it to finish
const run = await client.actor("luminar/meta-ads-library-creative-changefeed").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 = {
    "searchTerms": ["nike"],
    "maxAdsPerTarget": 15,
}

# Run the Actor and wait for it to finish
run = client.actor("luminar/meta-ads-library-creative-changefeed").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 '{
  "searchTerms": [
    "nike"
  ],
  "maxAdsPerTarget": 15
}' |
apify call luminar/meta-ads-library-creative-changefeed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,luminar/meta-ads-library-creative-changefeed"
        }
    }
}

```

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/YqaIcG9BMK77saZlu/builds/E7XW2NPSsPoQuuQ54/openapi.json
