# Facebook Ads Library Scraper (`tidyharvest/facebook-ads-library-scraper`) Actor

Meta Ad Library ads by page, keyword or link: text, media, dates, platforms, political spend, EU/UK reach included free. Built to beat Facebook Ad Library Scraper: ads for 57 of 58 test sources vs 45, zero template text vs 25%, 5x faster on a 40-source batch (Oct 2026). From $0.50 per 1,000.

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

## Pricing

Pay per event

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

## Facebook Ads Library Scraper

Get every ad a brand is running on **Facebook, Instagram, Messenger, Threads and Audience Network** from the **Meta Ad
Library**: ad text, headline, landing page, call to action, **images and video URLs**, carousel cards, start and end
dates, platforms, and the advertiser's page. Add **EU and UK transparency data** (reach, reach by country, age and
gender, targeting, payer and beneficiary) with one checkbox.

Paste a **Facebook page**, an **Ad Library URL** from your browser, a single **ad link**, or just type **search terms**
and countries. No login, no cookies, no Facebook account.

### Compared with Facebook Ad Library Scraper

We built this to beat [Facebook Ad Library Scraper](https://apify.com/curious_coder/facebook-ads-library-scraper)
(`curious_coder/facebook-ads-library-scraper`), the most-used independent Ad Library actor on Apify Store. In October 2026
we ran both on the **same 61 inputs** in two rounds (37 brand and small-advertiser pages from over a dozen countries, 15 keyword
searches with platform, media, sort and exact-phrase filters, 8 ad share links, 1 nonexistent ad), active ads only, all
countries, up to 300 (first round) or 100 (second round) ads per source.

| | This actor | Facebook Ad Library Scraper |
|---|---|---|
| Sources that returned ads (of 58 that have ads) | **57** | 45 |
| Brand pages it reported as "private" (Revolut, Allbirds, Hims, Heineken, Jack Daniel's) | **read 5 of 5** (707 ads) | 0 of 5 |
| Ad share links (`/ads/library/?id=...`) | **7 of 7** | 0 of 7 |
| Rows whose text is a template placeholder (`{{product.brand}}`) | **0%** (0 of 6,289) | 25% (1,558 of 6,149) |
| Inactive ads returned when you asked for active only | **0** | 21 |
| Ads on sources both read | the same ads (all but 1 of our 5,565 were in theirs) | 0-30 more per source, past the per-source cap |
| EU reach on the same 150 ads (first round) | identical values | identical values |
| Error rows charged | **none** (status rows are free) | 7 |
| EU/UK reach details | **included** | included |
| Price per 1,000 ads | **$0.50** ($0.35 on Gold and above), no start fee | $0.75 + $0.05 per run ($0.75 measured) |
| Time for 40 mixed pages, searches and ad links (up to 100 ads each; two runs each) | **83-95 s** | 433-525 s |

Pages whose ads Facebook partly hides from logged-out visitors (alcohol and other age-restricted brands) are read as
far as Facebook allows and say so: "Facebook shows 8 of about 364 ads to logged-out visitors".

**Where it still does more:** on a single source it is about as fast or a little faster (first round: a brand page took
70 s vs 66 s here; with EU details on, 180 ads took 33 s vs 27 s). Its developer documents a limit of roughly 3,000-6,000 ads per search URL; in our test this actor returned
10,920 ads from a single search.

The other big one, [Facebook Ads Library Scraper](https://apify.com/apify/facebook-ads-scraper) (`apify/facebook-ads-scraper`),
cost $4.30-5.00 per 1,000 ads on our plan tiers in the same tests (its card runs from $5.80 down to $1.00 at the top tier),
returned nothing for all five walled pages, and had placeholder text in 28% of rows (88 of 318).

### How well it works

Measured in October 2026 on Apify cloud at the default 256 MB, on **88 real inputs** drawn the way users bring them:
54 brand and small-advertiser pages in 15+ countries, 20 keyword searches in 10 languages (with media, platform, date,
sort, exact-phrase and political filters), 12 ad share links, and a nonexistent ad and page.

- **87 of 88 inputs answered correctly**: ads for 44 pages, 19 searches and all 12 share links; an honest status row for
  the rest (no ads running, a nonexistent ad or page, or a handle that matches several country pages, with their ids listed).
  One page lookup was throttled on every IP tried and says so.
- **15,991 ads, zero duplicates.** Ad text on 98.0%, landing page on 97.5%, an image or video on 98.8%, call to action on
  98.2% (the rest genuinely have none, or their media is hidden from logged-out visitors, which `mediaRequiresLogin` flags).
- **About $0.04 of platform cost per 1,000 ads** on these runs, so the price leaves room to keep it maintained.
- **Political ads:** spend range, impressions range and "paid for by" on 299 of 299 US election ads.
- **EU reach** on 95% of ads shown in the EU (171 of 180), matching the values the leading actor returns on every shared ad.
- **10,920 ads from one keyword search** in a single run, and a run moved by Apify to another server mid-way finished
  with 914 of 914 ads and no duplicates.
- **Dynamic and catalog ads** come back with the text the site shows, never a `{{product.name}}` placeholder.

### What you can do with it

- **Competitor monitoring:** every active ad of the brands you follow, daily. Turn on *Only new ads* and each run returns
  (and charges for) only ads you haven't seen.
- **Creative research / swipe files:** search ad text by keyword and country, filter by video or image, platform,
  language and date, sort by impressions.
- **Lead qualification:** *Count only* tells you whether a company is advertising on Meta and roughly how many ads it
  runs, for $1 per 1,000 companies.
- **EU/UK transparency and political ads:** reach and audience breakdowns for ads shown in the EU or UK; spend and
  impression ranges for issue, election and political ads.
- **One ad by its link:** paste `facebook.com/ads/library/?id=...` and get that ad.

### Input

| Field | What it does |
|---|---|
| `urls` | Any mix of Ad Library URLs, Facebook page URLs (`facebook.com/nike`, `profile.php?id=`, `/people/...`), page handles, numeric page ids and single-ad links. |
| `searchTerms` | Keywords to search ad text for. No URL needed. |
| `countries` | Two-letter codes (US, GB, DE...). Empty = all countries (or the country in your URL). Several = one search per country. |
| `maxAdsPerSource` | Stop each source after this many ads (default 1,000; 0 = all). |
| `maxItems` | Hard cap on ads for the whole run, which also caps your cost. |
| `activeStatus`, `adType`, `mediaType`, `platforms`, `startDate`, `endDate`, `languages`, `sortBy`, `exactPhrase` | Filters. They apply to **every** source, including Ad Library URLs you paste. |
| `includeAdDetails` | EU/UK reach, audience breakdowns, payer/beneficiary, verified legal entity and advertiser profile per ad (free). |
| `countOnly` | One row per source with the ad count and whether it is advertising (no ads returned). |
| `onlyNewAds` + `stateKey` | Scheduled monitoring: skip ads returned on earlier runs, so you pay only for new ones. |
| `onePerAdvertiser` | Keep the first ad of each page (lead lists from keyword searches). |
| `runTag` | Copied into every row. |

Field names other Ad Library actors use also work (`startUrls`, `resultsLimit`, `limitPerSource`, `count`,
`scrapeAdDetails`, `onlyTotal`...), so you can switch without rewriting your integration.

Examples:

```json
{ "urls": ["https://www.facebook.com/nike", "https://www.facebook.com/gymshark"], "countries": ["US"], "maxAdsPerSource": 0 }
```

```json
{ "searchTerms": ["protein powder"], "countries": ["US", "GB"], "mediaType": "video", "sortBy": "impressions", "maxAdsPerSource": 200 }
```

```json
{ "urls": ["https://www.facebook.com/ads/library/?active_status=active&ad_type=all&country=DE&q=versicherung&search_type=keyword_unordered"], "includeAdDetails": true }
```

### Output

One row per ad (`type: "ad"`), with stable camelCase field names and ISO dates:

```json
{
  "type": "ad",
  "adArchiveId": "1870984290951787",
  "adLibraryUrl": "https://www.facebook.com/ads/library/?id=1870984290951787",
  "pageId": "15087023444",
  "pageName": "Nike",
  "pageUrl": "https://www.facebook.com/nike/",
  "pageLikeCount": 39502064,
  "pageCategories": ["Sportswear"],
  "isActive": true,
  "startDate": "2026-09-02T07:00:00Z",
  "endDate": null,
  "activeDays": 45,
  "platforms": ["facebook", "instagram"],
  "displayFormat": "VIDEO",
  "body": "Ad text as shown...",
  "title": "Headline",
  "linkUrl": "https://www.nike.com/...",
  "linkDomain": "nike.com",
  "ctaText": "Shop now",
  "ctaType": "SHOP_NOW",
  "imageUrls": [],
  "videos": [{"hdUrl": "https://video...", "sdUrl": "https://video...", "previewImageUrl": "https://scontent..."}],
  "cards": [],
  "isDynamicCatalogAd": false,
  "collationCount": 3,
  "collationId": "...",
  "categories": [],
  "paidForBy": null,
  "spend": null,
  "impressions": null,
  "position": 1,
  "sourceTotalEstimate": 2722,
  "source": {"input": "https://www.facebook.com/nike", "type": "page", "url": "https://www.facebook.com/ads/library/?...", "pageId": "15087023444"},
  "scrapedAt": "2026-10-17T04:00:00Z"
}
```

- **`collationCount`**: the Ad Library groups near-identical ads ("3 ads use this creative and text"). This is that number.
- **Dynamic and catalog ads** (`isDynamicCatalogAd`): the ad's top-level text is a template (`{{product.name}}`). We
  return the first real variation's text instead, as the site shows it, never the placeholder. All variations are in `cards`.
- **`spend`, `impressions`, `paidForBy`**: Meta publishes these only for issue, election and political ads.
- With `includeAdDetails`, a `details` object adds `euTotalReach`, `euAudience` (targeted ages, gender, locations, reach by
  country/age/gender), the same for the UK, `payer`, `beneficiary`, `verifiedPayer`/`verifiedBeneficiary` (legal name,
  country, website) and `advertiser` (Instagram handle and followers, category, verification, description).
- **Status rows (free):** a source that finds nothing or can't be read gets one row with `type: "status"`
  (`no-ads`, `limited-by-facebook`, `page-not-found`, `ad-not-found`, `invalid-input`, `failed`) so every input has a row. Filter on
  `type = "ad"` to drop them, or turn off `includeStatusRows`.
- `SUMMARY` in the run's key-value store lists every search: ads returned, the site's estimate, pages read.

### Pricing

- **$0.50 per 1,000 ads** (`ad-scraped`) on the Free plan; $0.45 Bronze, $0.40 Silver, $0.35 Gold and above.
- **EU/UK details are included**: no extra charge when you turn them on. No start fee.
- **Count only: $1 per 1,000 sources** (`source-counted`).
- Status rows, duplicates and ads skipped by *Only new ads* or your caps are never charged. One ad found by two of your
  sources is returned and charged once.

### Reliability

- **Runs resume after Apify moves them to another server.** The run continues from where it stopped and never returns
  or charges an ad twice. (In our test, a run without this saved 2,896 duplicate rows out of 7,703 after one move.)
- **Empty results are double-checked.** When Facebook throttles an IP it answers with an empty result instead of an
  error. Before reporting "no ads", the actor checks a query that always has results; if that is empty too, it moves to
  a fresh IP and continues from the same page.
- **Filters are honoured** on every input type. Ads that don't match a filter you set are not returned or charged.

### Limits and notes

- **Estimates vs. results:** the Ad Library's "~N results" is Meta's estimate. Scrolling to the end usually shows fewer,
  and that's what any scraper can return. Our `sourceTotalEstimate` is Meta's number; `SUMMARY` shows how many were read.
- **Login-only ads** (some age-restricted or regulated categories) are not shown to logged-out visitors and can't be returned.
- **Media URLs** are Facebook CDN links that expire after a few days or weeks: download what you need to keep.
- **Speed:** each source pages through results in order (about 5-9 ads per second, as fast as Facebook answers), and up
  to 16 sources run in parallel at the default 512 MB (8 at 256 MB, 24 at 768 MB+). EU/UK details are fetched alongside, 32 at a time.

### Is it legal?

The Meta Ad Library is a public transparency database that Meta publishes so anyone can see the ads running on its
platforms. This actor reads it the way a logged-out visitor does: no login, no account, no private data. The ads and
pages are businesses' public advertising. Some fields name people (a page run by an individual, a political ad's
"paid for by" disclaimer, payer/beneficiary names): if you store or use those, data-protection law (GDPR, CCPA and
similar) applies to you. Ad creatives are copyrighted by their advertisers; research and analysis are generally fine,
republishing them is not. Meta's terms restrict automated collection; if in doubt, ask a lawyer about your use.

# Actor input Schema

## `urls` (type: `array`):

One per line, any mix of: an Ad Library search URL copied from your browser (facebook.com/ads/library/?...), a Facebook page URL (facebook.com/nike, profile.php?id=..., /people/...), a page handle (nike) or a numeric page id. A page returns all of its ads.

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

Keywords to search ad text for, one per line (e.g. running shoes). Each term is searched in every country below. No need to build a URL.

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

Two-letter country codes where the ads ran (US, GB, DE...). Empty = all countries, or the country in your URL. Several codes = one search per country.

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

Stop each URL, page or search term (per country) after this many ads. 0 = no limit (every ad the Ad Library shows).

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

Hard cap on ads for the whole run, which also caps your cost. Empty or 0 = no total cap.

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

Applies to every source, including pasted URLs.

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

Political and issue ads include spend and impression ranges. Applies to every source.

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

Applies to every source, including pasted URLs.

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

Only ads shown on these platforms. Empty = all.

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

YYYY-MM-DD. Same as the Ad Library's date filter: ads with impressions on or after this date.

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

YYYY-MM-DD. Ads with impressions on or before this date.

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

Order of results; with a per-source cap, decides which ads you get.

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

Match search terms as an exact phrase instead of any of the words.

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

Two-letter language codes of the ad text (en, es, de...). Empty = all.

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

Per ad: EU and UK reach, reach by country/age/gender, targeted age, gender and locations, payer and beneficiary, the verified legal entity behind the ad, and the advertiser's Instagram, category and description. Reach data exists only for ads shown in the EU or UK. One extra request per ad, fetched in parallel; included in the price.

## `countOnly` (type: `boolean`):

Return one row per source with Facebook's estimated ad count and whether it is running ads ($1 per 1,000 sources). Quick lead qualification: is this company advertising, and how much?

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

For scheduled monitoring: skip (and don't charge for) ads this actor already returned for the same source on earlier runs. Remembered in a named key-value store on your account.

## `stateKey` (type: `string`):

Keeps separate memories for different monitoring jobs when Only new ads is on. Default: default.

## `onePerAdvertiser` (type: `boolean`):

Keep only the first ad of each Facebook page (lead lists from keyword searches).

## `includeStatusRows` (type: `boolean`):

A free row with type "status" for each source that found no ads or could not be read, so every input has a row. Filter on type = "ad" to drop them.

## `includeRawData` (type: `boolean`):

Adds the untouched ad JSON as `raw` (and `rawDetails`) for fields we don't flatten.

## `runTag` (type: `string`):

Copied into every row, to tell batches apart downstream.

## `useApifyProxy` (type: `boolean`):

Recommended. Keeps big runs going when Facebook slows down requests.

## `residentialFallback` (type: `boolean`):

If Facebook keeps slowing down a source, try harder to finish it. Uses a little more paid proxy traffic (about 5 KB per ad).

## Actor input object example

```json
{
  "urls": [
    "https://www.facebook.com/nike"
  ],
  "countries": [],
  "maxAdsPerSource": 1000,
  "maxItems": 100,
  "activeStatus": "default",
  "adType": "default",
  "mediaType": "default",
  "sortBy": "default",
  "exactPhrase": false,
  "includeAdDetails": false,
  "countOnly": false,
  "onlyNewAds": false,
  "onePerAdvertiser": false,
  "includeStatusRows": true,
  "includeRawData": false,
  "useApifyProxy": true,
  "residentialFallback": true
}
```

# Actor output Schema

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

One row per ad.

# 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 = {
    "urls": [
        "https://www.facebook.com/nike"
    ],
    "countries": [],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidyharvest/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 = {
    "urls": ["https://www.facebook.com/nike"],
    "countries": [],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("tidyharvest/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 '{
  "urls": [
    "https://www.facebook.com/nike"
  ],
  "countries": [],
  "maxItems": 100
}' |
apify call tidyharvest/facebook-ads-library-scraper --silent --output-dataset

```

## MCP server setup

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