# Facebook Ad Scraper (`scraper-engine/facebook-ad-scraper`) Actor

Analyze Facebook Ad Library campaigns to uncover advertisers, active ads, creatives, copy, landing pages, and targeting signals. Build structured competitor intelligence datasets for ad research, market analysis, creative benchmarking, and performance strategy.

- **URL**: https://apify.com/scraper-engine/facebook-ad-scraper.md
- **Developed by:** [Scraper Engine](https://apify.com/scraper-engine) (community)
- **Categories:** Social media, Lead generation, Other
- **Stats:** 1 total users, 0 monthly users, 96.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$19.99/month + usage

To use this Actor, you pay a monthly rental fee to the developer. The rent is subtracted from your prepaid usage every month after the free trial period. You also pay for the Apify platform usage, which gets cheaper the higher Apify subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#rental-actors

## 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 Ad Library Intelligence Scraper

**Scrape Facebook & Instagram Ad Library ads by keyword or Page** — with automatic duplicate suppression, creative-format classification, ad-longevity analytics, and advertiser-scale tiering built on top of Meta's own Ad Library GraphQL API.

***

### 🚀 Why Choose This Actor?

- 🔄 **Automatic duplicate suppression** — Meta's own pagination can hand back the same ad twice across overlapping pages (verified live); this actor tracks `adId` per run and drops exact repeats before they reach your dataset
- 🎨 **Creative-format classification** — every ad is classified as `CAROUSEL` / `VIDEO` / `SINGLE_IMAGE` from its *actual* media payload (card/image/video counts), not just Meta's raw internal code, which mixes formats
- 📅 **Ad-longevity analytics** — every ad gets a computed `adDurationDays`, plus a run-level average, so you can see which creatives/advertisers keep running (a strong signal of "this is working for them")
- 🏢 **Advertiser-scale tiering** — every advertiser Page is bucketed micro → mega by like count, so you can separate small local advertisers from major brands in one pass
- 🔍 **Search or Page mode** — point it at an Ad Library search URL (keyword) or a Facebook Page URL directly
- 📊 **Run analytics** — a `RUN_SUMMARY` written to the key-value store: totals, dedup/filter counts, format & tier breakdowns, top advertisers by ad count

***

### ✨ Key Features

- 🎯 **Keyword or Page sources** — mix search URLs and Page URLs in the same run
- 🔄 **`deduplicateAds`** — on by default; skips any ad whose `adId` was already collected this run
- 🎨 **`creativeFormatFilter`** — only keep ads matching a chosen format (`CAROUSEL`, `VIDEO`, `SINGLE_IMAGE`, `UNKNOWN`)
- 🏢 **`minAdvertiserLikes`** — filter out low-authority/throwaway advertiser Pages
- 📦 **`fetchAdDetails`** — optional per-ad enrichment call that adds `advertiserDetails`, `transparencyByLocation`, `verifiedVoiceContext`, `violationTypes` when Meta exposes them
- 🌍 **Country/date filtering** — `dateRange` preset, per-source country codes via the source URL

***

### 📥 Input Configuration

```json
{
  "adLibrarySources": [
    { "url": "https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=US&q=shoes&search_type=keyword_unordered&media_type=all" }
  ],
  "proxyConfig": { "useApifyProxy": true },
  "maxAdsPerSource": 50,
  "fetchAdDetails": true,
  "dateRange": "",
  "pageAdsActiveStatus": "all",
  "pageAdsCountryCode": "ALL",
  "deduplicateAds": true,
  "creativeFormatFilter": [],
  "minAdvertiserLikes": 0
}
```

| Field | Type | Description |
|-------|------|-------------|
| `adLibrarySources` | `array` | 🔍 Ad Library search URLs or Facebook Page URLs (required) |
| `proxyConfig` | `object` | 🛡️ Apify Proxy config (required) |
| `maxAdsPerSource` | `integer` | 📊 Max ads to collect per source URL (0 = unlimited) |
| `fetchAdDetails` | `boolean` | 📦 Fetch an extra per-ad details call (slower, adds enrichment-only fields) |
| `dateRange` | `string` | 📅 Date preset for search sources (`LAST_90_DAYS`, `LAST_30_DAYS`, `LAST_7_DAYS`, `LAST_3_DAYS`, `TODAY`, or empty) |
| `pageAdsActiveStatus` | `string` | 🟢 Active status filter when scraping a Page URL directly (`all`/`active`/`inactive`) |
| `pageAdsCountryCode` | `string` | 🌍 Country filter when scraping a Page URL directly |
| `deduplicateAds` | `boolean` | 🔄 Skip already-seen `adId` values in this run (default `true`) |
| `creativeFormatFilter` | `array` | 🎨 Keep only ads with a computed format in this list (empty = keep all) |
| `minAdvertiserLikes` | `integer` | 🏢 Skip ads whose advertiser Page like count is below this (0 = disabled) |

***

### 📤 Output Format

```json
{
  "adId": "123456789012345",
  "advertiserName": "Example Shoe Co",
  "pageId": "987654321",
  "isActive": true,
  "startDateFormatted": "2026-06-01 00:00:00",
  "endDateFormatted": "2026-08-30 00:00:00",
  "adDurationDays": 90,
  "publisherPlatforms": ["FACEBOOK", "INSTAGRAM"],
  "creativeFormat": "CAROUSEL",
  "mediaAssetCount": 6,
  "advertiserScaleTier": "medium",
  "adLibraryUrl": "https://www.facebook.com/ads/library/?id=123456789012345",
  "spend": null,
  "reachEstimate": null,
  "creative": {
    "headline": "Step Into Summer",
    "bodyText": "New arrivals, 20% off this week only.",
    "ctaText": "Shop Now",
    "ctaType": "SHOP_NOW",
    "linkUrl": "https://example.com/summer",
    "carouselCards": ["…"],
    "pageLikeCount": 812345,
    "pageCategories": ["Shopping"]
  }
}
```

| Field | Description |
|-------|--------------|
| `adId` | ✅ Meta's `ad_archive_id` — the stable identifier used for deduplication |
| `advertiserName` | 🏢 Advertiser Page name |
| `isActive` | 🟢 Whether the ad is currently running |
| `startDateFormatted` / `endDateFormatted` | 📅 Human-readable start/end (see longevity note below) |
| `adDurationDays` | 📈 **New.** Days between start and end |
| `creativeFormat` | 🎨 **New.** `CAROUSEL` / `VIDEO` / `SINGLE_IMAGE` / raw Meta code / `UNKNOWN`, computed from the actual media payload |
| `mediaAssetCount` | 🖼️ **New.** Total cards + images + videos + extra media attached to the creative |
| `advertiserScaleTier` | 🏢 **New.** `micro` (<10K likes) → `small` → `medium` → `large` → `mega` (10M+), from the advertiser Page's like count |
| `publisherPlatforms` | 🌐 Platforms this ad runs on (Facebook, Instagram, Audience Network, Messenger, Threads) |
| `spend` / `reachEstimate` / `impressionsText` | 💰 Spend/reach/impressions — see note below on when these are actually populated |
| `creative` | 🎨 The full renamed creative object: headline, bodyText, ctaText/ctaType, linkUrl, carouselCards, images, videos, pageLikeCount, pageCategories, etc. |
| `advertiserDetails` / `transparencyByLocation` / `verifiedVoiceContext` / `violationTypes` | 📦 Present only when `fetchAdDetails` is on and Meta returns them for that ad |

A `RUN_SUMMARY` object is written to the key-value store at the end of every run: `totalAdsCollected`, `duplicatesSkipped`, `filteredByCreativeFormat`, `filteredByAdvertiserLikes`, `creativeFormatBreakdown`, `advertiserScaleTierBreakdown`, `avgAdDurationDays`, `topAdvertisersByAdCount`.

***

### 🔬 Honest notes on real data (verified against a live sample run)

- **`spend` / `reachEstimate` / `impressionsText` are `null` for the vast majority of ads.** This is not a bug — Meta's Ad Library only discloses spend/reach/impression ranges for ads Meta classifies as political or social-issue ads in regulated regions. A live sample of 11 ordinary commercial ads (Google, Flipkart, Meesho, Booking.com, etc.) had **zero** populated spend/reach/impression fields. Don't build spend-based analysis on top of this actor for ordinary commercial ads — use `creativeFormat`, `mediaAssetCount`, `adDurationDays`, and `advertiserScaleTier` instead, all of which were populated on every real ad tested.
- **`adDurationDays` means "days running so far" for an active ad, not a guaranteed final duration.** Meta's own `end_date` for a still-active ad is actually its last-indexed/crawl timestamp, not a real campaign end date — confirmed against real data where every active ad in a sample shared the same `end_date` (the scrape date). For an inactive ad, `end_date` is the true campaign end and `adDurationDays` is the real total lifetime.
- **`categories` is `['UNKNOWN']` on most ordinary ads** — Meta only populates this for certain regulated ad categories (housing, employment, credit, political). Use `creative.pageCategories` (the advertiser Page's own category) for general classification instead.

### 🛠️ Fixed vs. the original scraper (verified live)

This actor's Playwright/cookie-bootstrap pipeline had two bugs that made the original scraper **fail on every run**, discovered and fixed during development:

1. Facebook serves a JS "verify" challenge (HTTP 403, self-reloading page) before the real Ad Library page loads. The original code waited for `networkidle`, which never resolves because of the reload cycle — it timed out after 60s on every run. Fixed by waiting for DOM content first, then giving the challenge's reload a bounded window to finish.
2. After solving that challenge, the original code re-fetched the same URL with a plain HTTP client to discover GraphQL document IDs — but a plain HTTP client can't run the challenge's JS, so it hit the same wall a second time and found no data. Fixed by reusing the already-challenge-passed page content captured by the browser session instead of a second raw fetch.
3. Meta's GraphQL API returns HTTP 200 with a `{"errors": [...]}` body when rate-limited, which is invisible to HTTP-level retry logic. Added GraphQL-level rate-limit detection with backoff-and-retry.

All three are verified fixed against a live run that produced real ad data end-to-end.

***

### 🎯 Best Use Cases

- 🕵️ **Competitive ad intelligence** — see what creatives, formats, and durations competitors are running
- 📈 **Creative trend research** — track carousel vs. video adoption across an industry via `creativeFormat`
- 🏢 **Advertiser landscape mapping** — segment micro/local advertisers from mega brands with `advertiserScaleTier`
- ⏱️ **Longevity signal mining** — long-running ads (`adDurationDays`) are a proxy for "this creative is performing"

***

### ⚠️ Legal & Ethical Notice

- This actor only collects data from Meta's own **public** Ad Library, which exists specifically for public ad transparency
- Users are responsible for compliance with applicable laws and Meta's Ad Library Terms of Service
- Do not use collected data to target or harass any individual or organization

***

### ❓ Frequently Asked Questions

**Q: Why is `spend`/`reachEstimate` empty on my results?**
A: Meta only discloses these for political/social-issue ads in regulated regions. It's expected for ordinary commercial ads — see the honest notes above.

**Q: Can I scrape a specific Page's ads instead of a keyword search?**
A: Yes — pass the Page's Facebook URL as a source instead of an Ad Library search URL; the actor auto-detects the source type.

**Q: Does `fetchAdDetails` slow the run down?**
A: Yes, it adds one extra GraphQL call per ad (bounded to 5 concurrent requests). Turn it off if you only need the core fields.

***

### 📞 Support & Feedback

- 🐛 Report issues in the **Issues** tab
- 💡 Feature requests welcome
- ⭐ If this actor saves you time, please leave a review!

# Actor input Schema

## `adLibrarySources` (type: `array`):

One or more Facebook Ad Library search URLs (keyword searches) or Facebook Page URLs to pull ads from.

## `proxyConfig` (type: `object`):

Select proxies to be used by the crawler.

## `maxAdsPerSource` (type: `integer`):

Maximum number of ads to collect per source URL. Set to 0 for unlimited.

## `fetchAdDetails` (type: `boolean`):

If enabled, fetches an extra per-ad details request (slower but adds fields such as advertiserDetails, transparencyByLocation and violationTypes when Meta exposes them).

## `dateRange` (type: `string`):

Date preset for ads (search sources only).

## `pageAdsActiveStatus` (type: `string`):

Filter by active status when scraping a Page URL directly.

## `pageAdsCountryCode` (type: `string`):

Country code filter when scraping a Page URL directly (e.g. US, IN, ALL).

## `deduplicateAds` (type: `boolean`):

Skip ads whose adId (ad\_archive\_id) has already been collected in this run. Facebook's own pagination can return the same ad twice across overlapping pages; this suppresses exact repeats before they reach the dataset.

## `creativeFormatFilter` (type: `array`):

Only keep ads whose computed creativeFormat is in this list. Leave empty to keep every format. Values are computed from the actual creative payload (carousel cards / images / videos), not just Meta's raw display\_format code.

## `minAdvertiserLikes` (type: `integer`):

Skip ads whose advertiser Page like count is below this threshold. Useful for filtering out low-authority or throwaway pages when doing competitive research. Set to 0 to disable.

## Actor input object example

```json
{
  "adLibrarySources": [
    {
      "url": "https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=IN&q=linkedin&search_type=keyword_unordered&media_type=all"
    }
  ],
  "proxyConfig": {
    "useApifyProxy": true
  },
  "maxAdsPerSource": 10,
  "fetchAdDetails": true,
  "dateRange": "",
  "pageAdsActiveStatus": "all",
  "pageAdsCountryCode": "ALL",
  "deduplicateAds": true,
  "creativeFormatFilter": [],
  "minAdvertiserLikes": 0
}
```

# 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 = {
    "adLibrarySources": [
        {
            "url": "https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=IN&q=linkedin&search_type=keyword_unordered&media_type=all"
        }
    ],
    "proxyConfig": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scraper-engine/facebook-ad-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 = {
    "adLibrarySources": [{ "url": "https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=IN&q=linkedin&search_type=keyword_unordered&media_type=all" }],
    "proxyConfig": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scraper-engine/facebook-ad-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 '{
  "adLibrarySources": [
    {
      "url": "https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=IN&q=linkedin&search_type=keyword_unordered&media_type=all"
    }
  ],
  "proxyConfig": {
    "useApifyProxy": true
  }
}' |
apify call scraper-engine/facebook-ad-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scraper-engine/facebook-ad-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/VrzeR2eANXaZ7oyDI/builds/SL54Ne0EJdf6ViVmg/openapi.json
