# Facebook Ad Library Scraper (`otterfetch/meta-ad-library`) Actor

Every ad a brand runs on Facebook, Instagram and Threads, from the public Meta Ad Library, logged out. Or check in bulk if pages run Meta ads. Page URL, domain or brand input. $0.70 per 1,000 ads, $1 per 1,000 page checks, no start fee. Never more ads or charges than you asked. Empty searches free.

- **URL**: https://apify.com/otterfetch/meta-ad-library.md
- **Developed by:** [Otter Fetch](https://apify.com/otterfetch) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 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.

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

**Never more ads or charges than you asked for. Empty ad searches are free.**

A Facebook Ad Library scraper that works like a simple Meta Ad Library API: give it page URLs, website domains or brand names, and get every ad they run on Facebook, Instagram, Messenger, Audience Network and Threads as flat rows. Use it to track competitor Facebook ads, collect Instagram ads for creative research, or check if a brand runs Facebook ads right now for a lead list. It reads the public Ad Library without logging in. Not affiliated with Meta.

**Price:** $0.70 per 1,000 ads, $1 per 1,000 page checks. No start fee. Status and summary rows are free. Add-ons: EU details $0.30 / 1k ads with data, media archive $0.50 / 1k ads (images) or $2 / 1k (with video).

**What you get**

- Ads mode: one summary row per target ("40 of 5,547 shown"), then one row per ad with text, dates, days running, platforms, format, call to action, landing page, images and videos.
- Page check mode: one row per page with running yes or no, active ad count, verified flag, likes, Instagram followers and category, built for lead lists.
- Optional add-ons: EU reach and demographics and media archiving, so image and video links do not expire.

See the same brand's Google ads with our [Google Ads Transparency actor](https://apify.com/otterfetch/google-ads-transparency).

### Why this one

- **Hard limits.** `maxAdsPerTarget` and your maximum cost per run are never exceeded. The Actor works out what it can still pay for before it asks Meta for each page, so a big page never pushes you over.
- **Free when empty.** A search with no ads, a blocked target, a skipped target and an unclear domain each give a plain-English status row. None of them is billed.
- **You can see what you did not get.** Each target starts with a summary row such as "30 of 4,212 shown", so you know how much more there is before you raise the limit.
- **Exact when you give a page URL.** Domains and brand names are matched with a stated confidence, never guessed in silence.
- **Flat output.** One row per ad with the text, dates, platforms, landing page, images and videos as plain columns.
- **Media you can keep.** Meta's image and video links expire after about 4.4 days. You can archive the files into your run's storage.

### Quick start

1. Pick **Mode**: **Ads** or **Page check**.
2. Type your **Targets**, one per line. A Facebook page URL (`https://www.facebook.com/nike`) is best. A page ID, an Ad Library link, a website domain or a brand name also work. Or paste a CSV or Google Sheets link in **Import targets from a link**.
3. Set a country, filters and **Max ads per target** if you want. The default is 30, which is the fastest and cheapest. The form is prefilled with a small first run.
4. Click **Start**. When it ends, open the **Overview** view or export the dataset as CSV.

### What it returns

Every row has `type`, `input`, `status`, `row_id` and `ad_library_url`.

- **`target_summary`** (ads mode, one per target, first): the page or query that was used, Meta's full total, how many ads you got, and a label like `30 of 4,212 shown`. It also has the format mix, platforms, top landing domains, and `match_confidence` and `match_reason`. It is free.
- **`ad`** (ads mode, one per ad): ad ID and link, page, active flag, start and end dates, days running, platforms, display format, `body`, `body_is_template`, `body_template`, title, `title_template`, link description, call to action, landing URL and domain, images, videos, cards, `variant_count`, `variant_ad_ids`, spend and impressions where Meta shows them (political ads), EU data when you ask for it, `media_expires_at` and `archived_media`.
- **`page_check`** (page check mode, one per page): page name, ID, URL, verified flag, likes, Instagram name and followers, `running_ads`, `active_ad_count` (counts above 50,000 show as 50,000 with `count_capped` true), the newest start date, the oldest start date in the sample, the format mix, platforms and top landing domains. `extraction.page_info` says where the verified flag, Instagram fields and category came from (`missing` when Meta returned none; those fields are then empty).
- **`status`** (free): a `reason` and a plain `message` for anything empty or uncertain: `invalid_target`, `page_not_found`, `domain_low_confidence`, `no_ads`, `filters_too_narrow`, `blocked`, `failed`, `meta_changed`, `skipped_limit`, `skipped_budget` and `skipped_timeout`.

#### Example output

Ads mode, Gymshark (`maxAdsPerTarget` 40):

| type | page_name | shown_label | ad_id | start_date | days_running | display_format | platforms | body | cta_text | landing_domain | variant_count |
|---|---|---|---|---|---|---|---|---|---|---|---|
| target_summary | Gymshark | 40 of 5,547 shown | | | | | | | | | |
| ad | Gymshark | | 2429977260813766 | 2026-08-28 | 56 | DCO | INSTAGRAM | So hot, du wirst es nicht glauben. Hol dir jetzt ... | Learn more | gymshark.com | 4 |
| ad | Gymshark | | 1571293847690205 | 2026-07-05 | 90 | VIDEO | INSTAGRAM | Sois ready en ayant la tenue qui te donnera la motivation ... | Learn more | gymshark.com | 4 |
| ad | Gymshark | | 1353284589767881 | 2026-07-31 | 47 | VIDEO | INSTAGRAM | Ça sent une motivation à bloc et de nouveaux PRs ... | Learn more | gymshark.com | 3 |

Page check mode:

| input | page_name | running_ads | active_ad_count | verified | likes | ig_username | ig_followers | category | extraction.page_info |
|---|---|---|---|---|---|---|---|---|---|
| https://www.facebook.com/nike | Nike | true | 2,832 | true | 39,497,425 | nike | 290,912,401 | Sportswear Store | details |
| 111126298904665 | Potato Corner | false | 0 | true | 679,893 | potatocornerph | 90,587 | Food Stand | typeahead |

### Two modes

**Ads.** For each target you get a summary row, then its ads, newest, most relevant or most impressions first (your choice). Page 1 comes back fast (about 30 ads). More ads are fetched page by page until you reach `maxAdsPerTarget`, and the Actor stops there.

**Page check.** For each page you get one `page_check` row that answers: is this page running Meta ads right now, how many, and the newest and oldest start dates among its first ~30 ads. It makes one or two quick lookups per page and never walks through the ads, so it is quick and cheap for lead lists. It always counts active ads, whatever the active status filter says.

### Inputs

Page URL first: a Facebook page URL gives exact results. A page ID and an Ad Library link are exact too. Website domains and brand names are matched to a page, and the match comes with a `match_confidence` (`high`, `medium`) and a `match_reason`. If a domain cannot be matched with enough confidence, you get a free `domain_low_confidence` row that lists the best candidate pages, so you can pick one and run it by its page URL.

| Field (key) | What it does |
|---|---|
| Mode (`mode`) | `ads` or `pageCheck`. Default `ads`. |
| Targets (`targets`) | One per line: page URL (best), page ID, Ad Library link, website domain, or brand or keyword. |
| Import targets from a link (`importUrl`) | A CSV or Google Sheets link. First column. |
| Search brand names as keywords (`keywordMode`) | Off: a brand name is resolved to its page. On: the line is a keyword search across all advertisers. |
| Country (`country`) | Where the ads ran. Default all countries. |
| Active status (`activeStatus`) | `active`, `inactive` or `all`. Default `active`. |
| Media type (`mediaType`) | all, image, video, meme, image and meme, or none. |
| Platforms (`platforms`) | Facebook, Instagram, Messenger, Audience Network, Threads. |
| Language (`language`) | Ad language. |
| Ran between (`dateFrom`, `dateTo`) | Ads that were running at any time in this range. An ad that started before the range and was still live inside it is included. |
| Sort (`sort`) | `relevance`, `impressions` (most impressions) or `newest`. |
| Max ads per target (`maxAdsPerTarget`) | Hard limit on what is fetched and billed. Default 30. 0 means no limit. |
| One row per creative (`groupVariants`) | See below. |
| EU reach and demographics (`euDetails`) | See below. |
| Archive media (`archiveMedia`) | See below. |
| Max targets (`maxTargets`) | Safety limit for scheduled runs. Extra targets get a free `skipped_limit` row. |

An Ad Library link keeps its own filters (country, status, media type, platforms, language, dates, sort) for that target.

#### Fewer, cheaper rows: `groupVariants`

Advertisers often run the same creative many times with small changes. Turn on **One row per creative** and you get one row per group of variants, with `variant_count` and `variant_ad_ids`. It is billed once per group, so a brand with 200 variants of 20 creatives costs for 20 ads. `maxAdsPerTarget` then counts rows.

#### EU reach and demographics: `euDetails`

For ads shown in the EU or UK, Meta publishes total reach and the age, gender and country breakdown, plus who paid and who benefits. Turn this on to add them in the `eu` column. It costs $0.0003 per ad, and **only for ads where Meta actually returned EU or UK data**. Nothing is charged when the option is off, when the ad was not shown there, or when Meta had no data.

Tip: set the country to an EU country (for example France) together with this option. With all countries, the ads shown first are often not EU ads, so few rows get EU data.

#### Archive media: `archiveMedia`

Meta's image and video links are signed and stop working after about 4.4 days (`media_expires_at` tells you the exact time). To keep the files, set **Archive media** to `images` (600 px images and video preview frames) or `images_videos` (also the video file). The files are saved in the run's default key-value store and linked in `archived_media`. It is billed per archived ad, and not when a download failed.

Archived files follow your Apify storage retention, so they are removed when your plan's retention ends. To keep them longer, copy them somewhere else, for example to your own S3 or Google Cloud bucket.

#### Template text: `body_is_template`

Some ads (dynamic creative) hold placeholders in their text, such as `{{product.name}}`. Meta fills these in per viewer, and the public library shows the placeholder. For these ads `body_is_template` is true, and `body`, `title` and the landing link are taken from the first card of the ad that has real text (the product the ad shows). The raw placeholders stay in `body_template` and `title_template`. When no card has real text, the placeholder is kept as written; the Actor never makes up text.

### Pricing

Pay per event. You pay for results, not for time. No start fee.

| Event | Price | Charged when |
|---|---|---|
| `ad` | $0.0007 | One ad row with data was returned. Summary, status and failed rows are free. |
| `page-check` | $0.001 | One page was checked and you got its answer. Every verdict is charged, yes or no, including "not running any ads". Pages that could not be checked are not. |
| `ad-eu-details` | $0.0003 | On top of `ad`, only when `euDetails` is on and Meta returned EU or UK data for that ad. |
| `media-archive` | $0.0005 | On top of `ad`, when at least one image of the ad was stored. Not charged when the download failed. |
| `media-archive-video` | $0.002 | On top of `ad`, when a video file of the ad was stored. Charged instead of `media-archive` for that ad. |

Prices shown are the current list prices. Apify shows the live price on the Actor page.

A page check is billed for its yes-or-no answer. A page that is not running ads still gets a real answer, so it costs the same as a page that is. Empty ad searches in ads mode, status rows, summary rows, blocked or failed lookups and unclear domains are never billed.

What a run costs: 1,000 ads cost $0.70. With EU details on, add up to $0.30 per 1,000 ads, and with images archived add up to $0.50 per 1,000 ads (up to $2 per 1,000 ads instead with videos archived too). 1,000 page checks cost $1.

The Actor respects your maximum cost per run. It prices each ad at the most it could cost with the add-ons you turned on, and leaves the rest of your targets as free `skipped_budget` rows once the budget is used.

### Example inputs

An exact look at one brand:

```json
{
  "mode": "ads",
  "targets": ["https://www.facebook.com/nike"],
  "country": "US",
  "maxAdsPerTarget": 30
}
```

Which of these companies run Meta ads?

```json
{
  "mode": "pageCheck",
  "importUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit"
}
```

Fewer rows, with the files saved:

```json
{
  "mode": "ads",
  "targets": ["jollibee.com.ph"],
  "country": "PH",
  "groupVariants": true,
  "archiveMedia": "images",
  "maxAdsPerTarget": 50
}
```

### Limitations

- **No reach or impressions for most ads.** For ads that are not political and not shown in the EU or UK, Meta does not show reach or impressions to logged-out visitors, so we cannot return them.
- **Domains and brand names are matched.** A domain is matched through the ads that link to it. Marketplaces (Lazada, Shopee), affiliate-heavy sites and brands without a website often cannot be matched with confidence. Those targets are not fetched or billed. Use the page URL for exact results.
- **Large counts stop at 50,000.** Meta stops counting at 50,001, so higher counts show as 50,000 (`total_ads` 50000 with `total_capped` true and "50,000+" in the summary label, `count_capped` true on a page check). To get all the ads of a very large advertiser, split the run by date range.
- **Logged-out public data only.** The Actor sees what any visitor to the Ad Library sees. It does not log in, and it does not return anything Meta shows only to signed-in users.
- Meta changes the Ad Library from time to time. If it does, the Actor tries to update itself. Targets it cannot read get a free `meta_changed` row, and if no target can be read, the run stops before charging and says so.

### FAQ

**Do I need a Facebook account or cookies?** No. The Actor reads the public Meta Ad Library logged out, the same pages anyone can open. It never asks for a login.

**Is this the official Meta Ad Library API?** No. Meta's official API covers mainly political and issue ads and needs an approved developer account. This Facebook ads scraper reads the public Ad Library website for all ad types and gives you the same kind of structured rows.

**Does it include Instagram ads?** Yes. Every ad lists its platforms: Facebook, Instagram, Messenger, Audience Network and Threads. Filter by platform in the input or after export.

**How do I check if a brand runs Facebook ads?** Use Page check mode with a list of page URLs, domains or brand names. Each page gets one row with `running_ads`, `active_ad_count` and the page's verified flag, likes and Instagram followers. A page check costs $0.001, including when the answer is "not running any ads".

**Why did I get fewer ads than the total Meta shows?** You get at most `maxAdsPerTarget` ads per target (default 30). The summary row says how many exist, for example "30 of 4,212 shown". Raise the limit or set it to 0 for all ads (big brands can have thousands; set a maximum cost per run). Meta also counts some ads it no longer shows.

**Can I get spend and impressions?** Meta publishes spend and impressions only for political and issue ads, and the Actor returns them for those. For ads shown in the EU or UK, turn on EU details for reach by age, gender and country.

**What if I give a website domain instead of a page URL?** The Actor finds the page that runs ads for that domain and says how sure it is (`match_confidence` high or medium). If no page is a clear match, you get a free row listing the closest pages. A page URL is always exact.

**Why do image and video links stop working?** Meta's media links expire after about 4.4 days. Turn on media archiving to keep copies in your run's storage. The copies follow your Apify storage retention.

**How much does a run cost?** 1,000 ads cost $0.70. Page checks cost $1 per 1,000. Add-ons: EU details $0.30 / 1k ads with data, media archive $0.50 / 1k ads (images) or $2 / 1k (with video). Set a maximum cost per run and the Actor stops before it, never after. Rows for empty searches, blocked or skipped targets and unclear domains are free.

**Will I ever pay for more ads than my limit?** No. The Actor trims each page to what you are still owed before it saves the rows, and it charges only after a row is saved.

**What happens when my budget runs out mid-target?** The Actor delivers as many ads as the budget covers, stops, and marks the remaining targets as skipped. You are never charged beyond your maximum.

**What if a run is interrupted?** Restarts continue where the run stopped. Charges are never repeated after a restart, but a few free status rows may appear twice.

**Can AI agents use it?** Yes, through the Apify API or MCP server. Summary rows come first, every row has a `type`, and status rows explain in plain words why something is empty.

**Is this affiliated with Meta?** Not affiliated with Meta. Data comes from the public Meta Ad Library.

### More from us

See the same brand's Google ads with our [Google Ads Transparency actor](https://apify.com/otterfetch/google-ads-transparency).

### Issues

Report problems in the Issues tab. Include the run link and the target that went wrong.

# Actor input Schema

## `mode` (type: `string`):

Ads returns the ads each target runs. Page check answers one question per target: is it running Meta ads right now, how many, and the newest and oldest start dates among its first ~30 ads. Page check never walks through the ads. Every checked page is charged, including a page that is running no ads.

## `targets` (type: `array`):

One per line. Best: a Facebook page URL, which gives exact results. Also accepted: a page ID, an Ad Library link, a website domain (for example jollibee.com.ph) or a brand name or keyword. A page URL gives exact results; domains and brand names are matched and may be ambiguous. Required unless you give an import link.

## `importUrl` (type: `string`):

Optional https link to a CSV file or a Google Sheets link (shared as "anyone with the link"). One target per row in the first column; a header row named target, page, url, domain, brand or keyword is skipped. Example: https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit

## `keywordMode` (type: `boolean`):

Off: a brand name is matched to its Facebook page. On: the line is a keyword search across all advertisers.

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

Only ads that reached this country. All countries is the default.

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

Which ads to return: running now, stopped, or both.

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

Only ads with this kind of media.

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

Only ads shown on these platforms. Leave empty for all platforms.

## `language` (type: `string`):

Only ads written in this language, as detected by Meta.

## `dateFrom` (type: `string`):

Ads that were running at any time between this date and the end date (not only ads that started in the range). Format YYYY-MM-DD. Optional.

## `dateTo` (type: `string`):

End of the range. Format YYYY-MM-DD. Optional.

## `sort` (type: `string`):

Order of the ads. Meta does not offer an oldest-first order.

## `maxAdsPerTarget` (type: `integer`):

Hard limit per target: the run never fetches or bills more. The summary row shows the full total (for example '30 of 4,212 shown'); raise this to get more. 30 is the fastest and cheapest. 0 means no limit, so be careful with large brands.

## `groupVariants` (type: `boolean`):

Meta groups near-identical ads. On: one row per group with variant_count and variant_ad_ids, billed once. Off: every ad is its own row, as Meta lists them.

## `euDetails` (type: `boolean`):

Adds total reach and age, gender and country breakdowns for ads shown in the EU or UK, plus payer and beneficiary. Only ads where Meta returned this data are charged. Makes one extra request per ad. Works best with an EU country selected.

## `archiveMedia` (type: `string`):

Meta media links expire after a few days. Archiving saves the files in this run's key-value store so the links keep working. Priced per archived ad (see the pricing tab); failed downloads are not charged.

## `maxTargets` (type: `integer`):

Optional safety limit for scheduled runs. Extra targets are listed as skipped and not charged.

## Actor input object example

```json
{
  "mode": "ads",
  "targets": [
    "https://www.facebook.com/nike",
    "https://www.facebook.com/JollibeePhilippines/"
  ],
  "keywordMode": false,
  "country": "ALL",
  "activeStatus": "active",
  "mediaType": "all",
  "language": "any",
  "sort": "relevance",
  "maxAdsPerTarget": 10,
  "groupVariants": false,
  "euDetails": false,
  "archiveMedia": "off"
}
```

# Actor output Schema

## `overview` (type: `string`):

One row per target: Meta's full total, ads returned, page details. Start here in ads mode.

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

One row per ad with text, media, landing page, dates and platforms.

## `pageCheck` (type: `string`):

One row per target: whether it runs Meta ads and how many. Only in page check mode.

## `status` (type: `string`):

Free rows for targets with no ads, invalid lines, blocked or skipped targets, with what to try.

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

Targets per verdict, charged events, estimated cost and request statistics (key-value store record RUN_SUMMARY).

# 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 = {
    "mode": "ads",
    "targets": [
        "https://www.facebook.com/nike",
        "https://www.facebook.com/JollibeePhilippines/"
    ],
    "country": "ALL",
    "language": "any",
    "maxAdsPerTarget": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("otterfetch/meta-ad-library").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 = {
    "mode": "ads",
    "targets": [
        "https://www.facebook.com/nike",
        "https://www.facebook.com/JollibeePhilippines/",
    ],
    "country": "ALL",
    "language": "any",
    "maxAdsPerTarget": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("otterfetch/meta-ad-library").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 '{
  "mode": "ads",
  "targets": [
    "https://www.facebook.com/nike",
    "https://www.facebook.com/JollibeePhilippines/"
  ],
  "country": "ALL",
  "language": "any",
  "maxAdsPerTarget": 10
}' |
apify call otterfetch/meta-ad-library --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,otterfetch/meta-ad-library"
        }
    }
}
```

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/btMKP70bqZcAxoJWT/builds/IuSwyzprBUdGZXp45/openapi.json
