# Facebook Ad Library Scraper — Meta Ads, Winners & Monitor (`tactful_anvil/facebook-ads-library-scraper`) Actor

Scrape the Meta (Facebook & Instagram) Ad Library by keyword, advertiser name, page or URL: copy, creatives, landing pages, days running, winning score, EU/UK reach & targeting included. $0.60/1,000 ads.

- **URL**: https://apify.com/tactful_anvil/facebook-ads-library-scraper.md
- **Developed by:** [Mr Zack](https://apify.com/tactful_anvil) (community)
- **Categories:** Lead generation, Social media
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 ad scrapeds

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

## Facebook Ad Library Scraper — Meta Ads, Winners & Monitor

Scrape the **Meta Ad Library** (Facebook, Instagram, Messenger, Threads, Audience Network) by **keyword**, by **advertiser name** (just type "gymshark" or paste facebook.com/gymshark — no page-ID hunting), by **page ID**, by **single ad ID**, or by pasting any **Ad Library URL**. Every ad comes back as one flat row: ad copy, headline, CTA, landing page, images, videos, carousel cards, start date, **days running**, **versions**, **rank** in Meta's impressions order and a transparent **winning score**.

Every ad also comes with what Meta publishes under "See ad details" — **included in the price**: **reach** (EU/UK) broken down **by country, age and gender**, **targeting** (age, gender, locations), **payer & beneficiary**, and the advertiser's **Instagram handle, followers and verification**.

**$0.60 per 1,000 ads, details included.** Duplicates, filtered-out ads and ads you already received in monitor mode are **never charged**. No login, no cookies, no API key, no browser. Deep paging runs through Apify residential proxy automatically (≈ $0.035 of proxy traffic per 1,000 ads on your Apify bill — measured: 450 ads with details in 88 s for $0.016 total platform usage).

### Why this one

| | This Actor |
|---|---|
| Find an advertiser by **name / @handle / page URL** | ✅ resolved to the page ID automatically (exact match first, then verified / most-liked, alternatives in `RUN_SUMMARY`) |
| **Winning-ad signals** | ✅ `daysActive`, `versionsCount`, `rankInQuery` (Meta's impressions order) and a 0–100 `winningScore` on every row |
| **Sort** | ✅ impressions (Meta's default) or **newest first** for monitoring |
| **EU/UK audience data** | ✅ `includeAdDetails` (on by default, no extra charge): reach by country/age/gender, targeting, payer/beneficiary |
| **Flat output** | ✅ one clean row per ad, same columns every time — opens straight in Sheets/Excel, no nested `snapshot` to unpack |
| **Deep paging** | ✅ automatic: page 1 direct, deeper pages via residential proxy with IP rotation; parallel queries |
| **Monitor mode** | ✅ `onlyNewAds` — pay only for ads you haven't seen |
| **Dynamic/catalog ads** | ✅ `{{product.name}}` placeholders replaced by the real card copy |
| **Honest empties** | ✅ an empty result page is re-checked with a fresh session before we report "0 ads" |
| Price | $0.60 / 1,000 ads, details included |

### Who this is for

- **Media buyers & performance marketers** — find the ads that are *actually working*: an ad that has been running for 60+ days and has several versions is being paid for because it converts. Filter with `minDaysActive`.
- **E-commerce & dropshipping teams** — see which products competitors push, with the exact landing pages (`landingDomain`, `linkUrl`) and creatives.
- **Agencies** — pull a prospect's full ad footprint by page ID for a pitch deck, or schedule weekly competitor reports.
- **Creative strategists & copywriters** — build swipe files of hooks, headlines and CTAs by niche keyword.
- **AI agents (MCP)** — *"what ads is brand X running on Instagram right now, and which have run the longest?"* is one tool call with a predictable price.

### What you get per ad

| Field | Example | Notes |
|---|---|---|
| `adArchiveId`, `libraryUrl` | `925321173274919` | Open the ad in Meta's library |
| `pageName`, `pageId`, `pageUrl`, `pageLikes`, `pageCategories` | `Nike`, `15087023444` | The advertiser |
| `isActive`, `startDate`, `endDate`, `lastSeenDate` | `true`, `2026-08-03` | Run window |
| `daysActive` | `53` | Days the ad has been running (to today, or to its end date) |
| `isNew` | `false` | Started within `newWithinDays` (default 7) |
| `versionsCount` | `4` | How many near-identical versions Meta grouped together — a scaling signal |
| `platforms` | `facebook, instagram, threads` | Where it is shown |
| `displayFormat` | `video` / `image` / `carousel` / `dpa` / `dco` | Creative type (dpa/dco = catalog/dynamic ads) |
| `bodyText`, `title`, `linkDescription`, `caption` | | Ad copy (dynamic `{{product.name}}` placeholders are replaced by the real card copy) |
| `ctaText`, `ctaType` | `Shop Now`, `SHOP_NOW` | Call to action |
| `linkUrl`, `landingDomain` | `https://www.footlocker.com/…`, `footlocker.com` | Destination (Meta's `l.facebook.com` redirect is unwrapped) |
| `primaryImageUrl`, `primaryVideoUrl`, `imageUrls`, `videos` | | Creative media (HD + SD video, preview image) |
| `cards`, `cardCount` | | Carousel / catalog cards: title, body, link, image, video |
| `spend`, `impressions`, `currency`, `disclaimer` | | Filled for political & issue ads (Meta publishes these only there). `spend` = `{lower, upper, text}`, e.g. `{40000, 45000, "£40K-£45K"}` |
| `reachEstimate` | | Political & issue ads: Meta's reach bucket, e.g. `>1M` |
| `containsAiGeneratedMedia` | `false` | Meta's AI-media label |
| `rankInQuery` | `3` | Position in Meta's result order (impressions high → low by default) |
| `winningScore` | `78` | 0–100: long run (≤40) + versions (≤20) + top impressions rank (≤25) + multi-placement (≤10) + still active (5) |
| `reachTotal`, `reachRegion`, `reachByCountry`, `reachByAgeGender` | `14565`, `EU`, `{"DE":14235}` | With `includeAdDetails`, for ads shown in the EU/UK |
| `targetingAge`, `targetingGender`, `targetingLocations` | `18-65`, `All`, `Germany` | With `includeAdDetails` |
| `payer`, `beneficiary` | `ALLBIRDS, INC.` | With `includeAdDetails` (EU DSA disclosure) |
| `advertiserIgUsername`, `advertiserIgFollowers`, `advertiserVerification`, `advertiserAbout` | `allbirds`, `511773`, `BLUE_VERIFIED` | Free for `advertisers` inputs; for every ad with `includeAdDetails` |
| `searchQuery`, `searchCountry`, `scrapedAt` | | Which of your inputs produced the row |

Every field is always present — `null` when Meta does not publish it, never a made-up value.

A free **`RUN_SUMMARY`** record (key-value store) adds per query: Meta's reported total, ads delivered, pages fetched, why a query stopped; and **per advertiser**: ads, active ads, new ads, format mix, platforms, top landing domains, oldest start date and longest-running ad.

### Input

| Field | Default | What it does |
|---|---|---|
| `searchTerms` | `["nike"]` | Keywords / brands searched in all ad copy. Ad Library URLs pasted here work too |
| `advertisers` | `[]` | **Every ad of an advertiser** by name, `@handle`, facebook.com/… or instagram.com/… URL |
| `pageIds` | `[]` | Same, by numeric page ID (`view_all_page_id=…`) — pins the exact page |
| `adIds` | `[]` | Specific ads by Ad Library ID or `?id=` link |
| `adLibraryUrls` | `[]` | Paste a search URL from facebook.com/ads/library — all its filters are applied |
| `country` | `ALL` | ISO-2 country (US, GB, DE, BR, ID…) or ALL |
| `activeStatus` | `active` | `active`, `inactive` or `all` |
| `adType` | `all` | or `political_and_issue_ads`, `housing_ads`, `employment_ads`, `financial_products_and_services_ads` |
| `mediaType` | `all` | `image`, `video`, `meme`, `image_and_meme`, `none` |
| `platforms` | all | any of `facebook`, `instagram`, `messenger`, `audience_network`, `threads`, `whatsapp` |
| `languages` | all | ad language codes, e.g. `["en","es"]` |
| `exactPhrase` | `false` | match the search term as an exact phrase |
| `sortBy` | `impressions` | `impressions` (high → low, Meta's default) or `newest` |
| `includeAdDetails` | `true` | EU/UK reach by country/age/gender, targeting, payer/beneficiary, advertiser Instagram (included in the price) |
| `useResidentialForPaging` | `true` | Page past the first 30 ads per query through Apify residential proxy (≈ $0.035 / 1,000 ads proxy traffic) |
| `activeFrom` / `activeTo` | — | Meta's date filter: ads **shown** in this window (they may have started earlier) |
| `startedAfter` | — | only ads **launched** on/after this date (new campaigns) — filtered ads are free |
| `maxAdsPerQuery` | `100` | cap per search / page / URL |
| `minDaysActive` | `0` | only ads running at least N days (winning ads) — filtered ads are free |
| `newWithinDays` | `7` | window for the `isNew` flag |
| `onlyNewAds` | `false` | monitor mode — see below |
| `proxyConfiguration` | Apify Proxy | see "Depth & proxies" |

#### Example — winning ads in a niche

```json
{ "searchTerms": ["protein powder"], "country": "US", "minDaysActive": 30, "maxAdsPerQuery": 200 }
```

#### Example — everything two competitors run, with EU audience data

```json
{ "advertisers": ["gymshark", "https://www.instagram.com/allbirds/"], "country": "DE", "includeAdDetails": true }
```

#### Example — newest ads in a niche (daily monitor)

```json
{ "searchTerms": ["ai note taker"], "country": "US", "sortBy": "newest", "onlyNewAds": true }
```

### Schedule it: competitor monitor

Turn on **`onlyNewAds`** and schedule the Actor daily or weekly (Console → Schedules). The Actor remembers every ad ID it has seen per query (in a named key-value store in your account) and delivers — and charges — **only ads that are new since the last run**. Connect the dataset to Slack, email, Google Sheets or a webhook and you have a free-running "competitor launched a new ad" alert.

With **`sortBy: "newest"`** (recommended for monitoring) the Actor stops each query as soon as it reaches the ads it already delivered last time — so a quiet day costs almost nothing, and older ads that the previous run simply did not reach are never sold to you as "new".

Tip: with the default impressions sort, set `maxAdsPerQuery` at least as high as the number of ads you want to watch (e.g. an advertiser's full active count), so every run covers the whole set.

### For AI agents & MCP

- **Minimal input:** `{"advertisers": ["<brand name>"]}` or `{"searchTerms": ["<keyword>"]}`. Everything else has a sensible default.
- **Output:** one row per ad, flat JSON, stable field names (table above). `daysActive` + `versionsCount` answer "which ads work"; `landingDomain` answers "where does the traffic go".
- **Price is predictable:** $0.0006 per ad delivered, details included. Cap spend with `maxAdsPerQuery` or the run's max charge.
- **Empty is not an error:** a keyword with no ads ends SUCCEEDED with 0 rows and a clear status message. Short hiccups are retried inside the run (fresh sessions after 20 s and 45 s); only a Meta outage that survives every retry ends FAILED so your pipeline can alert. Short run timeouts end SUCCEEDED with whatever was collected.

### Depth & proxies (honest limits)

Meta serves the first page of every search (up to 30 ads) to anyone, but **rate-limits deeper paging from datacenter IPs**. The Actor handles this for you: page 1 and ad details go direct (free), deeper pages go through **Apify residential proxy** with automatic IP rotation when an exit IP gets limited. That traffic is small (≈ 0.4 MB per 100 ads) and appears on your Apify bill as residential proxy usage (≈ $0.035 per 1,000 ads). If your plan has no residential proxy, you still get the first page of every query and the status message says so. You can also plug in your own proxy in *Proxy configuration*.

Other limits: Meta shows spend/impressions only for political & issue ads, and reach/targeting data only for ads delivered in the EU or UK. Deleted ads and ads Meta hides from the library cannot be returned.

### FAQ

**Is this legal?** The Actor reads only the public Ad Library that Meta publishes for transparency, without logging in. You are responsible for how you use the data (e.g. GDPR if you process personal data of EU persons).

**Why is `bodyText` empty on a few ads?** Some image-only or dynamic ads have no copy text. Their cards (`cards`) usually still carry title and link.

**Why do I get fewer ads than Meta's reported total?** `maxAdsPerQuery`, filters (`minDaysActive`, `startedAfter`) and datacenter paging limits all reduce the count; `RUN_SUMMARY.queries[].limitedBy` tells you which one applied.

### Related Actors

- **Brand Ads Spy — Google + LinkedIn** (`tactful_anvil/brand-ads-cross-network-spy`) — the same brand across Google and LinkedIn ad libraries.
- **Ad Budget Signals** (`tactful_anvil/ad-budget-signals`) — does a list of domains run ads at all? One row per domain.
- **Google Ads Transparency Scraper** (`tactful_anvil/google-ads-transparency-scraper`).

### Changelog

- **0.2.1** (2026-09-28) — hardening from a 24-case cloud test matrix: runs with short timeouts now deliver (reserves scale with the run), 256 MB runs no longer run out of memory, one ad-details session per route (much less proxy traffic), one ad without details no longer switches details off for the rest of the run, monitor mode stops at last run's ads (newest sort), political `spend` and `reachEstimate` filled again (Meta switched them to text), dead `targetedCountries` column removed, patient retries before any failure, watchdog finishes the run before a platform timeout.

- **0.2.0** (2026-09-25) — advertiser name/URL lookup, single ads by ID, `sortBy` newest, `rankInQuery` + `winningScore`, ad details included (EU/UK reach, targeting, payer/beneficiary, advertiser Instagram), automatic residential paging with IP rotation, 3 queries in parallel, empty-page re-checks.

- **0.1.0** (2026-09-25) — first release: keyword / page / URL search, SSR + GraphQL paging with automatic route fallback, dynamic-ad placeholder fix, winning-ad filter, `startedAfter`, monitor mode, per-advertiser summary.

# Actor input Schema

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

Keywords searched across all ad copy, e.g. "running shoes", "crm software", "weight loss". One Ad Library search per term. Ad Library URLs pasted here work too.

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

Every ad of these advertisers. Type a brand name ("gymshark"), an @handle, or paste facebook.com/<page> or instagram.com/<handle> — the Actor finds the page ID for you (exact match first, then verified / most-liked).

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

Numeric Facebook page IDs to pull EVERY ad of one advertiser (find it in any Ad Library link as view_all_page_id=…). Profile URLs with ?id= also work.

## `adLibraryUrls` (type: `array`):

Paste search URLs copied from facebook.com/ads/library — all filters in the URL (country, media type, platforms, dates, page) are applied as-is.

## `adIds` (type: `array`):

Look up specific ads by their Ad Library ID (the number in facebook.com/ads/library/?id=…) or paste those links.

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

ISO-2 country where the ads were delivered (US, GB, DE, ID, BR…) or ALL for every country.

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

active = running now (default), inactive = stopped, all = both.

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

all = every ad. The special categories carry extra transparency data (spend/impressions ranges for political ads).

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

Filter by creative type.

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

Only ads shown on these platforms. Any of: facebook, instagram, messenger, audience_network, threads, whatsapp. Empty = all.

## `languages` (type: `array`):

ISO-639-1 codes of the ad copy language, e.g. en, es, de. Empty = all.

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

Search terms must appear as an exact phrase (Meta's "keyword_exact_phrase"). Off = any order.

## `sortBy` (type: `string`):

impressions = Meta's default, highest-impression ads first (best for finding winners). newest = most recently launched first (best for monitoring).

## `activeFrom` (type: `string`):

YYYY-MM-DD. Meta's date filter ("impressions by date"): only ads that were SHOWN on/after this day — they may have started earlier.

## `activeTo` (type: `string`):

YYYY-MM-DD. Meta's date filter: only ads that were shown on/before this day.

## `startedAfter` (type: `string`):

YYYY-MM-DD. Only ads whose first day (startDate) is on/after this day — i.e. NEW campaigns. Applied by this Actor after fetching; ads filtered out are not charged.

## `maxAdsPerQuery` (type: `integer`):

Stop each search/page/URL after this many unique ads. You pay only for ads delivered.

## `minDaysActive` (type: `integer`):

Only deliver ads that have been running at least this many days — long-running ads are the ones making money. Filtered ads are not charged. 0 = off.

## `newWithinDays` (type: `integer`):

An ad is flagged isNew=true when it started within this many days.

## `onlyNewAds` (type: `boolean`):

Remembers every ad ID per query across runs (named key-value store). Schedule daily/weekly and receive — and pay for — only ads that are new since the last run. Best with Sort order = newest: each query stops as soon as it reaches last run's ads.

## `includeAdDetails` (type: `boolean`):

Opens each ad's "See ad details": total reach and reach by country / age / gender (ads shown in the EU or UK), targeting age, gender and locations, payer & beneficiary, advertiser Instagram handle, followers and verification. Included in the price — turn off only for faster runs.

## `useResidentialForPaging` (type: `boolean`):

Meta blocks paging (ads 31+) from datacenter IPs. With this on, the Actor pages through Apify's residential proxy automatically: about $0.035 of proxy traffic per 1,000 ads, billed by Apify on your plan. Page 1 and ad details always use the free direct route. Off = max ~30 ads per query.

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

Optional. Leave empty for the automatic setup above. Select RESIDENTIAL (or your own proxy URLs) to route everything through it.

## Actor input object example

```json
{
  "searchTerms": [
    "nike"
  ],
  "advertisers": [],
  "pageIds": [],
  "adLibraryUrls": [],
  "adIds": [],
  "country": "ALL",
  "activeStatus": "active",
  "adType": "all",
  "mediaType": "all",
  "platforms": [],
  "languages": [],
  "exactPhrase": false,
  "sortBy": "impressions",
  "maxAdsPerQuery": 100,
  "minDaysActive": 0,
  "newWithinDays": 7,
  "onlyNewAds": false,
  "includeAdDetails": true,
  "useResidentialForPaging": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

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

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchTerms": [
        "nike"
    ],
    "advertisers": [],
    "pageIds": [],
    "adLibraryUrls": [],
    "adIds": [],
    "platforms": [],
    "languages": [],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("tactful_anvil/facebook-ads-library-scraper").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"],
    "advertisers": [],
    "pageIds": [],
    "adLibraryUrls": [],
    "adIds": [],
    "platforms": [],
    "languages": [],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("tactful_anvil/facebook-ads-library-scraper").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"
  ],
  "advertisers": [],
  "pageIds": [],
  "adLibraryUrls": [],
  "adIds": [],
  "platforms": [],
  "languages": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call tactful_anvil/facebook-ads-library-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tactful_anvil/facebook-ads-library-scraper"
        }
    }
}
```

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/sDeYsraUVh9XftHEw/builds/cCIX12dPBqWw5Al5y/openapi.json
