# Meta Ad Library Scraper - Facebook & Instagram Ads (`zkill3r/meta-ad-library-scraper`) Actor

Scrape the Meta Ad Library: Facebook & Instagram ads by keyword, advertiser page or Ad Library URL. Ad copy, CTA, landing page, media, run dates, platforms, plus spend, impressions, demographics and regions for political ads. No login.

- **URL**: https://apify.com/zkill3r/meta-ad-library-scraper.md
- **Developed by:** [Zee Lex](https://apify.com/zkill3r) (community)
- **Categories:** Marketing, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.55 / 1,000 ads

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?

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 Scraper

Scrape ads from the public **Meta Ad Library** (facebook.com/ads/library), covering Facebook, Instagram, Messenger and Audience Network ads. Search by keyword, get every ad from an advertiser's page, or paste any Ad Library URL from your browser. **No login and no Facebook account needed.**

You get the ad copy, headline, call to action, landing page, images, videos, carousel cards, platforms, run dates and number of ad versions. For political and issue ads you also get spend and impression ranges, the "paid for by" disclaimer, and demographic and region breakdowns.

Built for **reliability**: every ad is validated before saving, rate limits are retried with backoff on fresh sessions, and the run **fails with a clear message** instead of quietly returning empty or partial data. A search that truly has no ads finishes successfully with a clear "No ads found" status.

### Use cases

- **Competitor ad research**: see every ad a competitor is running, how long each has run, and where it sends traffic
- **Creative inspiration**: collect ad copy, headlines, images and videos that are working in your niche
- **Dropshipping product research**: spot products many advertisers are pushing, and long-running ads (a sign they're profitable)
- **Agency reporting**: track clients' and competitors' ad activity on a schedule
- **Political ad transparency**: monitor spend, reach and audience breakdowns of political and issue ads

### Input

Provide at least one of `searchTerms`, `pageIds`, `pageUrls` or `startUrls`.

| Field | Type | Default | Notes |
|---|---|---|---|
| `searchTerms` | string\[] | – | Keyword search in ad text |
| `pageIds` | string\[] | – | Advertiser page IDs (most reliable) |
| `pageUrls` | string\[] | – | Advertiser page URLs; the page ID is looked up |
| `startUrls` | URL list | – | Ad Library URLs copied from your browser, all filters kept |
| `country` | string | `ALL` | 2-letter ISO code (MY, US, GB…) or ALL |
| `adType` | string | `all` | `all` or `political_and_issue_ads` |
| `activeStatus` | string | `active` | `active`, `inactive` or `all` |
| `platforms` | string\[] | all | `facebook`, `instagram`, `messenger`, `audience_network` |
| `mediaType` | string | `all` | `all`, `image`, `video`, `meme`, `none` |
| `startDate` / `endDate` | YYYY-MM-DD | – | Ad delivery date range |
| `maxItems` | integer | `100` | Total ads across all inputs |
| `includePoliticalDetails` | boolean | `true` | Demographic/region breakdown for political ads |
| `downloadMedia` | boolean | `false` | Save images/videos to the key-value store |
| `proxyConfiguration` | object | Apify Proxy, RESIDENTIAL | Residential strongly recommended |

The filter fields apply to `searchTerms`, `pageIds` and `pageUrls`. `startUrls` keep their own filters.

#### Example: keyword search in Malaysia

```json
{
    "searchTerms": ["air fryer"],
    "country": "MY",
    "activeStatus": "active",
    "maxItems": 50
}
```

#### Example: all active ads of one brand

```json
{
    "pageUrls": ["https://www.facebook.com/nike"],
    "pageIds": ["15087023444"],
    "country": "ALL",
    "maxItems": 200
}
```

Tip: the page ID is in the Ad Library URL after `view_all_page_id=` when you open a brand's ads.

#### Example: a URL copied from the Ad Library

```json
{
    "startUrls": [
        { "url": "https://www.facebook.com/ads/library/?active_status=active&ad_type=all&country=US&media_type=video&q=skincare&search_type=keyword_unordered" }
    ],
    "maxItems": 100
}
```

### Output sample

One dataset item per ad:

```json
{
    "adArchiveId": "1111111111111111",
    "adLibraryUrl": "https://www.facebook.com/ads/library/?id=1111111111111111",
    "pageId": "15087023444",
    "pageName": "Philips Malaysia",
    "pageProfileUrl": "https://www.facebook.com/PhilipsMalaysia/",
    "pageLikeCount": 254000,
    "isActive": true,
    "startDate": "2025-10-01T00:00:00.000Z",
    "endDate": "2025-10-31T00:00:00.000Z",
    "platforms": ["facebook", "instagram"],
    "displayFormat": "IMAGE",
    "ctaText": "Shop now",
    "ctaType": "SHOP_NOW",
    "linkUrl": "https://shopee.com.my/philips",
    "displayUrl": "shopee.com.my",
    "title": "Philips Airfryer 3000 Series",
    "bodyText": "Crispy food with 90% less fat. Shop the Philips Airfryer now!",
    "images": ["https://scontent.xx.fbcdn.net/v/...jpg?oh=...&oe=..."],
    "videos": [],
    "cards": [],
    "collationCount": 3,
    "countries": [],
    "reachEstimate": null,
    "political": null,
    "scrapedAt": "2026-10-27T09:10:00.000Z"
}
```

Carousel ads fill `cards`: `[{ "title", "body", "linkUrl", "caption", "ctaText", "imageUrl", "video": { "hdUrl", "sdUrl", "previewImageUrl" } }]`.

Video ads fill `videos`: `[{ "hdUrl", "sdUrl", "previewImageUrl" }]`.

Political and issue ads fill `political`:

```json
{
    "spend": { "lower": 100, "upper": 199, "text": "100-199" },
    "impressions": { "lower": 1000, "upper": 5000, "text": "1K-5K" },
    "currency": "MYR",
    "paidBy": "Paid for by Example Party",
    "demographicBreakdown": [{ "ageRange": "25-34", "gender": "male", "share": 0.35 }],
    "regionBreakdown": [{ "region": "Selangor", "share": 0.55 }],
    "detailsCollected": true
}
```

Field notes:

- `collationCount` is the number of ad versions Meta groups under this ad.
- `countries` and `reachEstimate` are filled only where Meta publishes them (political ads, and ads delivered in the EU).
- `linkUrl` is the real landing page; Facebook's redirect wrapper is removed.
- `shares` in breakdowns are fractions (0.35 = 35%).
- `detailsCollected: false` means the breakdown page couldn't be read; the rest of the ad is still saved.
- With `downloadMedia` on, each ad also gets `storedMedia: [{ "kind", "key", "sourceUrl", "storeUrl" }]`.

### Media URLs expire

Image and video links from Facebook are **signed and expire**, usually within days. If you need the files later, either download them soon after the run or turn on `downloadMedia`, which copies up to 5 files per ad into the run's key-value store.

### Pricing

Pay per result: **$0.70 per 1,000 ads** on the Free plan, with lower prices on paid Apify plans (Bronze $0.65, Silver $0.60, Gold and above $0.55 per 1,000). There are no extra platform-usage charges, and you only pay for ads saved to the dataset.

### Cost tips

- Set `maxItems` to what you actually need. Results load about 30 ads per scroll.
- Narrow with `country`, `activeStatus`, `mediaType` and dates instead of scraping everything and filtering later.
- Use `pageIds` instead of `pageUrls` to skip the page-lookup step.
- Turn off `includePoliticalDetails` if you don't need breakdowns; it costs one extra page load per political ad.
- Leave `downloadMedia` off unless you need the files.

### Reliability and run status

- Ads without an archive ID, advertiser name or Ad Library URL are dropped and counted, never saved. Ads are deduplicated by archive ID across all inputs.
- Login walls, checkpoints, HTTP 403/429, Facebook rate-limit errors (e.g. code 1675004) and empty results where results were expected are detected as blocks. Each is retried on a fresh session with increasing backoff, up to 4 times.
- The run **fails** with a clear message when:
  - no ads were saved although results were expected,
  - 20 blocks happen in a row, or
  - at least 25% of requests were lost.
- A political ad whose breakdown page stays blocked is still saved without the breakdown, so it doesn't count as lost.
- `RUN_SUMMARY` (linked from the run's Output tab) lists saved and dropped ads, missing-field percentages, block reasons and unresolved page URLs.

### Limitations

- The Actor reads Facebook's own web data, which Meta can change at any time. Rising missing-field percentages in `RUN_SUMMARY` are the early warning.
- Facebook rate-limits aggressively. Datacenter proxies are usually blocked; use RESIDENTIAL proxies. Concurrency is deliberately low (2 pages at a time).
- Looking up a page ID from a page URL can fail when Facebook shows a login screen. Use `pageIds` in that case.
- Dynamic/catalog (DPA, DCO) and carousel ads keep their media inside `cards`; their top-level `images`/`videos` can be empty.
- Spend, impressions, demographic and region breakdowns exist only for political and issue ads (and EU-delivered ads where Meta publishes reach). They are not available for ordinary commercial ads.
- Meta shows at most a few thousand ads per search. Use narrower keywords or filters for more.
- Only publicly visible Ad Library data is collected. Meta's terms restrict automated collection; for official, documented access, Meta also offers the Ad Library API for political/issue ads and EU ads. You are responsible for how you use the data.

### Development

```bash
npm install
npm test        # offline parser tests against saved Ad Library responses
apify run       # live run using storage/key_value_stores/default/INPUT.json
```

Offline end-to-end check (serves the fixtures through the browser, no network):

```bash
npm run build
META_MOCK=1 APIFY_CHROME_EXECUTABLE_PATH=/path/to/chrome node dist/main.js
```

# Actor input Schema

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

Keywords to search in ad text, e.g. "air fryer". Each term is searched separately.

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

Facebook page URLs of advertisers, e.g. https://www.facebook.com/nike. The Actor looks up each page's ID. If a page can't be resolved, use Page IDs instead.

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

Numeric Facebook page IDs. Most reliable way to get all ads of one advertiser. Find it in the Ad Library URL after view_all_page_id=.

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

URLs copied from facebook.com/ads/library in your browser. All filters in the URL are kept; the filter fields below are ignored for these.

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

Two-letter ISO country code (e.g. MY, US, GB) or ALL.

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

Which ad category to search: all ads, or only political/issue ads (these include spend, impressions and audience breakdowns).

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

Show only active ads, only inactive ads, or all ads.

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

Only show ads running on these platforms. Leave empty for all.

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

Filter by creative type (images, videos, memes, or no media).

## `startDate` (type: `string`):

Only ads delivered on or after this date (YYYY-MM-DD).

## `endDate` (type: `string`):

Only ads delivered on or before this date (YYYY-MM-DD).

## `maxItems` (type: `integer`):

Maximum number of ads to save across all inputs.

## `includePoliticalDetails` (type: `boolean`):

For political and issue ads, open each ad's details to collect demographic and region breakdowns. One extra page load per political ad.

## `downloadMedia` (type: `boolean`):

Save ad images and videos to the key-value store. Media URLs from Facebook expire, so turn this on if you need the files later. Uses more traffic and time.

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

RESIDENTIAL proxies are strongly recommended. Facebook rate-limits datacenter IPs quickly.

## Actor input object example

```json
{
  "searchTerms": [
    "air fryer"
  ],
  "country": "ALL",
  "adType": "all",
  "activeStatus": "active",
  "mediaType": "all",
  "maxItems": 100,
  "includePoliticalDetails": true,
  "downloadMedia": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One item per ad.

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

Saved and dropped counts, missing-field percentages and anti-bot block reasons.

# 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": [
        "air fryer"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zkill3r/meta-ad-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": ["air fryer"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("zkill3r/meta-ad-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": [
    "air fryer"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call zkill3r/meta-ad-library-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zkill3r/meta-ad-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/leMbXHKJLpNeDjMde/builds/aqX4rysXV9epxqiAf/openapi.json
