# Meta Ad Library Scraper - Facebook Ads & Scaled Offers (`scrapewise/meta-ad-library-scraper`) Actor

Scrape the Meta Ad Library (Facebook, Instagram, Messenger) without login: ads by keyword, advertiser page or Ad Library link, with text, video, landing page, start date, days running and duplicated creatives, plus a scaled-offer ranking grouped by landing page.

- **URL**: https://apify.com/scrapewise/meta-ad-library-scraper.md
- **Developed by:** [Scrapewise Data](https://apify.com/scrapewise) (community)
- **Categories:** Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.43 / 1,000 ad delivereds

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

Meta Ad Library Scraper turns the public **Facebook Ad Library** (Facebook, Instagram, Messenger, Audience Network and Threads ads) into clean JSON, CSV or Excel, **without login, cookies or an API token**. Search any keyword in any country, list every ad of an advertiser page, or paste an Ad Library link with your own filters. Each ad comes with its text, title, call to action, landing page, images and video links, start date, **days running** and **how many ads reuse the same creative**.

On top of the ads, the Actor ranks the **scaled offers** it found: one row per landing page and advertiser that shows a scale signal (2+ active ads or 2+ copies of a creative), with how many ads are live, how many creative copies are running and for how long. It is the "offer mining" that media buyers, dropshippers, affiliates and info product sellers do by hand in the Ad Library, done in one run.

**US$ 0.50 per 1,000 ads. US$ 1.00 per 1,000 scaled-offer rows (optional). No monthly fee, no start fee. Error rows are free.**

### At a glance

- **Price per 1,000 ads:** US$ 0.50
- **Scaled-offer ranking:** Yes, one row per landing page, ranked by a scale score
- **Days running and creative copies per ad:** Yes (`daysRunning`, `sameCreativeAdsCount`)
- **Filter by minimum days running:** Yes (`minDaysRunning`)
- **Input:** Keywords x countries, advertiser pages, or any Ad Library link
- **Login, cookies, Meta API token:** None
- **Daily "what is new" mode:** Yes, new ads and new advertiser pages since the previous run (`compareWithPreviousRun`)
- **Error rows (blocked, no results, bad input):** Free, with an `errorCode`

### One real row

A keyword search for `emagrecer` in Brazil, video ads running for at least 14 days (collected on 2026-09-30, trimmed):

```json
{
  "type": "ad",
  "adArchiveId": "1320765540221734",
  "adLibraryUrl": "https://www.facebook.com/ads/library/?id=1320765540221734",
  "pageName": "Victor Pareto",
  "isActive": true,
  "startDate": "2026-06-20",
  "daysRunning": 103,
  "sameCreativeAdsCount": 1,
  "platforms": ["FACEBOOK", "INSTAGRAM", "AUDIENCE_NETWORK", "MESSENGER", "THREADS"],
  "displayFormat": "VIDEO",
  "title": "Queime gordura e ganhe massa muscular com o treino híbrido!",
  "ctaType": "LEARN_MORE",
  "linkUrl": "http://m.timehibrido.com.br/rt/desafio/quiz-novo",
  "landingDomain": "timehibrido.com.br",
  "hasVideo": true,
  "searchQuery": "emagrecer",
  "country": "BR"
}
```

And the top of the scaled-offer ranking from the same run:

| # | Landing domain | Advertiser | Active ads | Ads incl. creative copies | Longest running |
|---|---|---|---|---|---|
| 1 | cariani.com.br | Renato Cariani | 2 | 28 | 23 days |
| 2 | pilates.laystrancoso.com | Lays Sant'anna | 5 | 17 | 134 days |
| 3 | mercadolivre.com.br | Vichy | 10 | 11 | 48 days |

### What you can do with it

- **Find winning offers.** Search your niche, keep ads running for 14+ days, and read the offer ranking: an advertiser paying for 28 copies of the same creative is scaling something that sells.
- **Spy on competitors.** Put their page ids in `pageIds` and schedule the run daily: every new ad, every creative they keep on, every landing page they test.
- **Catch clones of your offer.** Schedule a daily search with your offer name and get only the ads and advertiser pages that appeared since yesterday (see below).
- **Build a swipe file.** Export text, titles, calls to action and video links of the ads that last longest in any country.
- **Feed AI agents and dashboards.** Stable field names, one row per ad, free error rows with codes.

### Input

| Field | Type | What it does |
|---|---|---|
| `searchQueries` | list of strings | Keywords in any language. Each one is searched in each country. |
| `pageIds` | list of strings | Numeric page ids, or Ad Library links with `view_all_page_id`. All ads of that advertiser. |
| `startUrls` | list of links | Any `facebook.com/ads/library` search link; its own filters are kept. |
| `countries` | list of strings | Two-letter codes (US, BR, GB, MX, FR...). Empty = all countries. |
| `activeStatus` | `active`, `inactive`, `all` | Default `active`. |
| `mediaType` | `all`, `image`, `video`, `meme`, `none` | Default `all`. |
| `exactPhrase` | boolean | Exact phrase instead of any word. |
| `minDaysRunning` | integer | Only ads running for at least N days. Filtered ads are not charged. |
| `maxItems` | integer | Max ads per keyword and country, page or link. Default 100. |
| `groupByOffer` | boolean | Add the scaled-offer ranking at the end. Default on. |
| `compareWithPreviousRun` | boolean | Mark new ads and new advertisers against the previous run and add a free summary row per search. Default off. |
| `onlyNew` | boolean | With the comparison on, deliver (and charge) only ads not seen before. Default off. |
| `snapshotName` | string | Name of the saved comparison. Empty: derived from the searches and countries. |

#### Examples

```json
{ "searchQueries": ["weight loss", "keto"], "countries": ["US", "GB"], "minDaysRunning": 14, "mediaType": "video", "maxItems": 300 }
```

```json
{ "pageIds": ["112565745050415"], "countries": ["BR"], "maxItems": 500, "groupByOffer": false }
```

### Daily clone and new-advertiser alerts

Info products, courses and low ticket offers get cloned fast: same name, same creatives, same emoji, on a handful of new pages, often the day after an offer starts to scale. Finding them in the Ad Library means searching by hand every morning and remembering what was there yesterday. This mode does the remembering.

Schedule the Actor once a day (Apify Console, **Schedules**) with your offer name, the words of your ad or your page, and the comparison on:

```json
{
  "searchQueries": ["your offer name", "a sentence from your ad"],
  "countries": ["BR", "PT", "MX"],
  "activeStatus": "active",
  "maxItems": 200,
  "compareWithPreviousRun": true,
  "onlyNew": true,
  "snapshotName": "my-offer-clones",
  "groupByOffer": false
}
```

What you get on each run:

- Every ad row gets `isNewAd` (not seen in any earlier run of this comparison), `firstSeenAt` and `isNewAdvertiser` (its page never showed up for this search before).
- One **`comparison_summary`** row per search and country, **never charged**: `newAdsCount`, `newAdvertisersCount`, `newAdvertisers` (page name, page id, page link, Ad Library link of the page, active ads found, new ads found), `droppedAdsCount` and `droppedAdIds` (ads of the previous run that did not show up now), and a one-line `message`. The **New since last run** table view shows only what matters.
- With `onlyNew`, ads you already saw are read but not delivered and not charged, so a quiet day costs close to nothing.
- The **first run** saves the baseline: everything comes as new and the summary says `first run: baseline saved`.

Send the dataset or the summary row to Slack, e-mail, Google Sheets or a webhook with an Apify integration and the alert arrives without you opening the Ad Library.

Notes: the state lives in a named key-value store in your own Apify account (`meta-ads-<snapshotName>`), one entry per search and country, and is saved only after the ads are delivered, so a failed run never erases it. Use a different `snapshotName` per schedule. Ids that disappear stay remembered for 60 days, so an ad that leaves the top results and comes back is not "new" again. "Dropped" means not seen in this run, which can also be because it fell beyond `maxItems`; keep `maxItems` the same every day. With the comparison off (the default), the output is exactly as before.

### Output

Two kinds of rows, told apart by `type`, with a table view for each (**Ads** and **Scaled offers**).

#### Ad rows (`type: "ad"`)

| Field | What it is |
|---|---|
| `adArchiveId`, `adLibraryUrl` | Ad id and its public Ad Library link |
| `pageId`, `pageName`, `pageUrl`, `pageLikeCount`, `pageCategories` | The advertiser page |
| `isActive`, `startDate`, `endDate`, `daysRunning` | Status and dates; `daysRunning` counts until today for live ads |
| `sameCreativeAdsCount`, `collationId` | How many ads share this creative, as the Ad Library groups them |
| `platforms` | FACEBOOK, INSTAGRAM, MESSENGER, AUDIENCE\_NETWORK, THREADS |
| `displayFormat` | IMAGE, VIDEO, CAROUSEL, DPA (catalog), DCO (dynamic creative)... |
| `bodyText`, `title`, `linkDescription`, `ctaText`, `ctaType` | The ad copy. Catalog placeholders like `{{product.name}}` are replaced by the first card's real text |
| `linkUrl`, `landingDomain`, `caption` | Where the ad sends people |
| `cards`, `cardsCount` | Carousel and catalog cards: title, text, link, image, video |
| `imageUrls`, `videoUrls`, `hasVideo` | Media links (Meta signs them: download within a few hours) |
| `containsAiContent`, `categories`, `disclaimer`, `spend`, `currency`, `impressions` | As shown by the Ad Library; spend and impressions exist only for political and social issue ads |
| `searchQuery`, `country`, `scrapedAt` | Which search produced the row, and when |

#### Scaled-offer rows (`type: "offer"`)

| Field | What it is |
|---|---|
| `rank`, `scaleScore` | Only offers with 2+ active ads or 2+ creative copies. Position and score: 3 points per active ad, 2 per extra copy of a creative, up to 9 for age (capped at 90 days) |
| `landingDomain`, `landingType` | The landing domain; ads that send to Instagram, WhatsApp, Messenger or Facebook are grouped by advertiser and `landingType` says where |
| `pageId`, `pageName`, `pageUrl` | The advertiser |
| `adsFound`, `activeAdsFound`, `adsSharingCreatives`, `videoAds` | Counts over the ads found in this run |
| `oldestStartDate`, `maxDaysRunning` | Age of the offer |
| `exampleAdUrl`, `exampleTitle`, `exampleBody`, `exampleLinkUrl` | The most duplicated, longest running ad of the offer |
| `searchQueries`, `platforms` | Where it showed up |

#### Comparison summary rows (`type: "comparison_summary"`, never charged)

Only with `compareWithPreviousRun`. One per search and country.

| Field | What it is |
|---|---|
| `searchQuery`, `country`, `snapshotName` | Which search and which saved comparison |
| `isFirstRun`, `previousRunAt` | First run of this comparison (baseline saved), or when the previous one ran |
| `adsSeen`, `advertisersSeen` | Ads and advertiser pages seen in this run |
| `newAdsCount`, `newAdvertisersCount`, `newAdvertisers` | What appeared since the previous run; each new advertiser with `pageName`, `pageId`, `pageUrl`, `adLibraryPageUrl`, `activeAdsFound`, `adsFound`, `newAdsFound` |
| `droppedAdsCount`, `droppedAdIds` | Ads of the previous run not seen now (up to 200 ids) |
| `searchComplete`, `message` | False when the search stopped early (block, timeout, spending limit); a one-line summary |

#### Error codes (never charged)

| errorCode | Meaning |
|---|---|
| `NO_RESULTS` | No ad for that search, country and filters |
| `BLOCKED` | Meta stopped answering after retries with new IPs; the ads already delivered stay |
| `INVALID_PAGE` | The page is not a numeric id or an Ad Library link with `view_all_page_id` |
| `INVALID_URL` | The link is not a `facebook.com/ads/library` link |
| `INVALID_INPUT` | A filter has a value the Actor does not know |
| `NOT_REACHED` | The run timeout arrived before this search |
| `ITEM_UNREADABLE`, `UNEXPECTED` | One ad or one search came in a shape the Actor cannot read; only that part is lost |

### Pricing

| Event | Price | Per 1,000 |
|---|---|---|
| Ad delivered | US$ 0.0005 | US$ 0.50 |
| Scaled-offer row | US$ 0.001 | US$ 1.00 |

A 300 ad search costs US$ 0.15, plus about US$ 0.07 for its offer ranking (69 offers with a scale signal in our `emagrecer` test in Brazil). Duplicates, filtered ads and error rows are free, and there is no Apify compute bill on top.

### How to use

#### Through the API

```bash
curl -X POST "https://api.apify.com/v2/acts/scrapewise~meta-ad-library-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"searchQueries":["running shoes"],"countries":["US"],"minDaysRunning":7,"maxItems":200}'
```

#### Schedules, integrations and AI agents

Schedule a daily run on a competitor's page and send new rows to Google Sheets, Slack or a webhook. Through the Apify MCP server, an AI agent can call it with plain JSON and read the offer ranking directly.

### Limitations

- **What Meta shows to a logged-out visitor.** The Actor reads the same public Ad Library a browser does. Reach, spend and demographics exist only for political and social issue ads and for ads delivered in the EU, as on the site.
- **The offer ranking covers the ads of this run.** It counts the ads the run collected, so a larger `maxItems` gives a fuller picture of each offer.
- **Media links expire.** Meta signs image and video links; download them within a few hours.
- **Meta changes the Ad Library often.** When a field moves the Actor fails loudly with an error code instead of returning empty rows. Open an issue and it gets fixed.
- This Actor collects only public ads and respects the site's terms. Advertiser pages are businesses and public figures; no personal data of ad viewers exists in the Ad Library.

### Changelog

- **2026-09-30, 0.1 (update):** daily comparison mode: `compareWithPreviousRun`, `onlyNew` and `snapshotName`; `isNewAd`, `firstSeenAt` and `isNewAdvertiser` on each ad; free `comparison_summary` row per search with new advertiser pages and dropped ads. Default output unchanged; prices unchanged.
- **2026-09-30, 0.1:** first release. Keyword, page and link searches in any country, active or inactive, by media type; days running, creative copies, landing domain, cards, media links; `minDaysRunning` filter; scaled-offer ranking; free error rows.

# Actor input Schema

## `searchQueries` (type: `array`):

Words to search in the Ad Library, one per line, in any language (running shoes, emagrecer, curso de inglês). Each keyword is searched in each country and returns up to 'Max ads per search'.

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

Numeric Facebook page ids or Ad Library links with view\_all\_page\_id, one per line. Returns the ads of that advertiser.

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

Paste any facebook.com/ads/library search link, with the filters you set on the site. The link's own filters win over the ones below.

## `countries` (type: `array`):

Two-letter country codes (US, BR, GB, MX, FR...). Each keyword is searched in each country. Empty means all countries (ALL).

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

Which ads to return.

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

Only ads with this kind of media.

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

Match the keyword as an exact phrase instead of any of its words.

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

Only ads running for at least this many days. Ads that stay on for weeks are usually the ones making money. 0 keeps every ad.

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

Upper limit of ads for each keyword and country, page or link.

## `groupByOffer` (type: `boolean`):

After the ads, add one 'offer' row per landing page (domain and advertiser) that shows a scale signal: 2 or more active ads, or 2 or more ads reusing the same creative. Each row has the active ads, creative copies and how long it has been running, ranked by a scale score. Charged per offer row; turn it off to get ads only.

## `compareWithPreviousRun` (type: `boolean`):

Made for daily schedules: each ad gets isNewAd, firstSeenAt and isNewAdvertiser, and each search adds one free 'comparison\_summary' row with the new ads, the new advertiser pages (name, id, link, active ads) and the ads of the previous run that are gone. The first run only saves the baseline. State is kept in a named key-value store in your account.

## `onlyNew` (type: `boolean`):

With 'Compare with previous run' on, deliver and charge only ads not seen before. Known ads are still read (so 'Max ads per search' keeps the same depth every day) but do not appear and are not charged.

## `snapshotName` (type: `string`):

Name of the saved comparison (stored as meta-ads-<name>). Use one name per schedule. Empty: derived from the keywords, pages, links and countries, so the same input always compares with itself.

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

Apify Proxy is recommended. If Meta blocks the datacenter IPs, the Actor moves to residential proxies on its own.

## Actor input object example

```json
{
  "searchQueries": [
    "running shoes"
  ],
  "countries": [
    "US"
  ],
  "activeStatus": "active",
  "mediaType": "all",
  "exactPhrase": false,
  "minDaysRunning": 0,
  "maxItems": 100,
  "groupByOffer": true,
  "compareWithPreviousRun": false,
  "onlyNew": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `resultsCsv` (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 = {
    "searchQueries": [
        "running shoes"
    ],
    "countries": [
        "US"
    ],
    "maxItems": 100,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapewise/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 = {
    "searchQueries": ["running shoes"],
    "countries": ["US"],
    "maxItems": 100,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapewise/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 '{
  "searchQueries": [
    "running shoes"
  ],
  "countries": [
    "US"
  ],
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapewise/meta-ad-library-scraper --silent --output-dataset

```

## MCP server setup

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