# Meta Ad Library Spy — Winning & Scaled Facebook Ads | $4/1K (`bovi/meta-ads-library-scraper`) Actor

Collect public Meta Ad Library ads by keyword or page ID. Get ad records with creative text, media URLs, delivery signals and platforms. Run with keywords or page IDs; no Meta login or Graph API token required. Pricing: $0.004 per ad on Free.

- **URL**: https://apify.com/bovi/meta-ads-library-scraper.md
- **Developed by:** [Vitalii Bondarev](https://apify.com/bovi) (community)
- **Categories:** Marketing, Social media, MCP servers
- **Features:** Limited permissions
- **Stats:** 155 total users, 30 monthly users, 98.4% runs succeeded, 4 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.88 / 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

Collect public Meta Ad Library ads by keyword or advertiser page ID, with copy, media URLs, delivery signals and platforms; no Meta account or token is required. Enable Apify Proxy with the `RESIDENTIAL` group: without it, Meta can return blocked or empty pages, and Free-plan runs deliver at most 50 ads.

- Find scaled creatives with `is_scaled`, `collation_count`, `days_active` and `longevity_score`.
- Monitor competitor ads by page ID or search term across countries, including active and inactive ads.
- Build creative swipe files with copy, CTAs, destination links and inline image or video URLs.
- Research political and issue ads with spend, impression, reach and EU transparency fields.
- Export flat records containing advertiser, creative, delivery and platform data.

### What you get

A field is `null` when the source does not provide it; arrays can be `null` or `[]` as noted.

| Field family | Fields | Type | What it contains |
|---|---|---|---|
| Ad identity | `ad_archive_id`, `ad_snapshot_url`, `page_id`, `page_name` | string | IDs, snapshot URL and advertiser; each can be `null`. |
| Advertiser profile | `page_profile_uri`, `page_profile_picture_url`, `page_like_count`, `page_categories`, `byline` | string, integer, array | Profile, audience, categories and disclaimer; profile values and `byline` can be `null`; categories can be `[]`. |
| Delivery signals | `is_active`, `ad_delivery_start_date`, `ad_delivery_stop_date`, `total_active_time`, `days_active`, `longevity_score` | boolean, ISO 8601 string, number, integer | Boolean status; dates are `YYYY-MM-DD` or `null`; `total_active_time` is passed through or `null`. Positive numeric duration gives `days_active = total_active_time // 86400`; otherwise it uses start to stop or run date, never below 0. `longevity_score` is `round(min(days_active / 90, 1.0), 3)`. |
| Scale signals | `is_scaled`, `collation_count`, `collation_id` | boolean, integer, string | `is_scaled` is true when `collation_count` is 3 or more; the other fields can be `null`. |
| Ad copy | `ad_creative_bodies`, `ad_creative_link_titles`, `ad_creative_link_captions`, `ad_creative_link_descriptions` | array of strings | Body, headlines, captions and descriptions; arrays can be `null`. |
| Destination and CTA | `link_url`, `cta_text`, `cta_type`, `display_format` | string | Landing page, call-to-action and format; each can be `null`. |
| Inline media | `snapshot_images`, `snapshot_videos`, `media_count`, `card_count` | array of strings, integer | Image/video URLs and counts; URL arrays can be `null`; counts are set, including `0`. |
| Platforms | `publisher_platforms` | array of strings | `FACEBOOK`, `INSTAGRAM`, `MESSENGER`, `AUDIENCE_NETWORK`, `THREADS`; `[]` when none are provided. |
| Ad metrics | `currency`, `spend_lower`, `spend_upper`, `spend_midpoint`, `impressions_lower`, `impressions_upper`, `impressions_midpoint`, `reach_estimate`, `has_metrics` | string, integer, boolean | Published spend, impressions and reach; metric integers and `reach_estimate` can be `null`; reach uses the lower bound. |
| Targeting and trust | `targeted_or_reached_countries`, `contains_digital_created_media`, `contains_sensitive_content`, `categories`, `entity_type` | array, boolean, string | Targeting, content and classification; arrays are `[]` when empty; scalar values can be `null`. |
| Transparency | `state_media_run_label`, `regional_regulation_data` | string, object | State-media and regional disclosures; either can be `null`. |
| Data quality | `parse_confidence`, `warnings` | number, array of strings | Parsing confidence and warnings; `warnings` is `[]` or can contain `missing_ad_archive_id`, `missing_page_name`, `missing_creative`, `missing_start_date` or `no_media_or_link`. |
| Run metadata | `scraped_at` | ISO 8601 string | One UTC scrape time for every row in a run; always set. |

### Sample output

The sample is shortened to selected fields from one dataset record.

```json
{
  "ad_archive_id": "388120997678322",
  "ad_snapshot_url": "https://www.facebook.com/ads/library/?id=388120997678322",
  "page_id": "1497383683837785", "page_name": "adidas",
  "is_scaled": false, "collation_count": null, "collation_id": null,
  "days_active": 601, "longevity_score": 1, "is_active": false,
  "ad_delivery_start_date": "2024-09-26", "ad_delivery_stop_date": "2026-05-20",
  "ad_creative_bodies": ["Conheça e compre todas as coleções e os principais lançamentos da adidas pelo app!"],
  "link_url": "http://play.google.com/store/apps/details?id=com.adidas.app",
  "cta_text": "Shop now", "cta_type": "SHOP_NOW", "display_format": "DPA",
  "media_count": 6, "card_count": 6,
  "publisher_platforms": ["FACEBOOK", "INSTAGRAM", "AUDIENCE_NETWORK", "MESSENGER"],
  "has_metrics": false, "parse_confidence": 1, "warnings": [],
  "scraped_at": "2026-11-04T05:29:04.147984+00:00"
}
```

### Pricing

This Actor uses pay-per-event pricing. Prices below are for the Free plan.

| Event | Price |
|---|---:|
| `apify-actor-start` | $0.00005 per event on the Free plan |
| `apify-default-dataset-item` | $0.004 per event on the Free plan |

The `apify-actor-start` event is charged once per GB of run memory, minimum one event. No price is shown for either the `ad-record` or `ad-record-political` event.

On the Free plan, 1,000 ads cost $4.00 and 10,000 ads cost $40.00, plus the start event(s) of each run.

You pay only for the listed events. This Actor does not use Pay per event + usage, so compute, proxy traffic and storage are not billed separately.

The $5 Free-plan credit covers 24 full runs (1,200 ads) at the 50-ad cap; the exact Free-plan result calculation is 1,249 ads assuming one start event.

### How to use

1. Enter `searchTerms`, numeric `pageIds`, or both.
2. Choose two-letter country codes; each term or page ID is searched for each country.
3. Select the ad category, active status, media type and keyword match type.
4. Set `maxResults` per search and country, and `pageSize` for streaming batches.
5. Enable Apify Proxy with the `RESIDENTIAL` group in `proxyConfiguration`.
6. Run the Actor, review the records, and repeat through Apify Schedules; daily is a starting point.

```json
{
  "searchTerms": ["nike"],
  "countries": ["US"],
  "adType": "all",
  "activeStatus": "all",
  "mediaType": "all",
  "matchType": "broad",
  "maxResults": 100,
  "pageSize": 25,
  "proxyConfiguration": {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}
}
```

Export the dataset as JSON or CSV, or read it through the Apify dataset API.

### Input parameters

| Parameter | What it does | Default |
|---|---|---|
| `searchTerms` | Keywords; each runs separately. If this and `pageIds` are empty, the run uses `nike`. | Empty |
| `pageIds` | Numeric advertiser page IDs, searched for every country. | Empty |
| `countries` | Two-letter codes, trimmed and upper-cased. Invalid codes are skipped with a run-log warning; `US` is used if none remain. | `["US"]` |
| `adType` | `all` or `political_and_issue_ads`. | `all` |
| `activeStatus` | `all`, `active` or `inactive`. | `all` |
| `mediaType` | `all`, `image`, `video`, `meme` or `none` (text-only). | `all` |
| `matchType` | `broad` or `exact` phrase matching for search terms. | `broad` |
| `maxResults` | Maximum per term or page ID per country; 1–10000. Other values use the nearest limit; non-numeric values use 100. | `100` |
| `pageSize` | Ads per streaming batch; 1–100. Other values use the nearest limit; non-numeric values use 25. | `25` |
| `docId` | Persisted-query ID; empty extracts the live ID on each page. | Empty |
| `proxyConfiguration` | Apify Proxy; a URL is requested per search attempt when set. | `{"useApifyProxy": false}` |
| `disableUsageStats` | Turns off the run summary described in Usage statistics when `true`. | `false` |

### Limits and edge cases

| Condition | What you see | What to do |
|---|---|---|
| Free Apify plan | At most the smaller of `maxResults` and 50 ads are delivered in total. | Use a paid Apify plan for uncapped output. |
| Several terms, page IDs or countries | Countries, then terms, then page IDs are processed; duplicate ad IDs are removed across the run. | Use distinct searches for separate result sets. |
| Every search is blocked or empty | The run succeeds with an empty dataset; the status is a sentence and `OUTPUT.emptyReason` gives the reason. `SOURCE_EMPTY` means only seen ads or the Free cap produced no new rows. | Broaden the search, try another country, use `activeStatus=all`, check filters, or enable `RESIDENTIAL`. |
| A search uses a proxy on its first page | Up to 4 attempts are made. | Enable `RESIDENTIAL` and run again later. |
| A search has no proxy on its first page | Up to 2 attempts are made, reusing the same address. | Enable `RESIDENTIAL` and run again later. |
| A proxy attempt is made | A fresh proxy URL is requested on each attempt. | Enable `RESIDENTIAL` and run again later. |
| `RESIDENTIAL` sees 2 clean pages with no ads | The search stops and is reported as `NO_MATCH_FOR_INPUT`. | Check the keyword, page ID, country and filters. |
| Every attempt for a search fails | The search is reported as `BLOCKED`. | Enable `RESIDENTIAL` and run again later. |
| An empty page is returned without `RESIDENTIAL` | The search is reported as `BLOCKED`. | Enable `RESIDENTIAL` and run again later. |
| A first-page failure occurs | Only a first-page failure raises the block. | Review the status and rerun the search. |
| The cost guard is checked before an attempt | The search can end early. | Try fewer inputs or rerun later. |
| A later page fails or returns no ads | That search ends; ads already read are kept and the final partial batch is delivered. It is not retried. | Review the dataset and rerun the affected search if needed. |
| A batch reaches `pageSize` rows | The batch is pushed to the dataset. | Use `pageSize` to control held rows. |
| A search finishes with a partial batch | The final partial batch is pushed. | Use a smaller `pageSize` for smaller final batches. |
| Fewer than `pageSize` rows are held | Held rows are neither pushed nor charged. | Use a smaller `pageSize`. |
| A row is pushed | It is charged immediately after the push. | Review the dataset and charging records. |
| An abort, timeout or crash occurs | Pushed rows stay; held rows are not written. | Use a smaller `pageSize`. |
| An abort, timeout or crash occurs before completion | No `OUTPUT` record is written because it is written only at the end. | Review rows already pushed. |
| Cost protection stops a partial run | Delivered ads remain; `MARGIN_GUARD` is written to the default key-value store and the status says results are partial. | Review the partial results and rerun with fewer inputs if needed. |
| Cost protection stops a run with ads | `OUTPUT.stoppedEarly = "MARGIN_GUARD"` and the run finishes normally. | Review the partial results. |
| Cost protection stops a run with search failures | `OUTPUT.searchFailures` lists the failures. | Review the listed failures. |
| Cost protection stops a run with zero ads | `OUTPUT.emptyReason = "BLOCKED"` and `stoppedEarly = "MARGIN_GUARD"`; the run fails. | Enable `RESIDENTIAL` and rerun later. |

### FAQ

#### How much does it cost to scrape Meta Ad Library ads?

On the Free plan, `apify-default-dataset-item` costs $0.004 per event and `apify-actor-start` costs $0.00005 per event. On the Free plan, 1,000 ads cost $4.00 plus the start event(s) of each run.

#### Why did my run return no ads?

A blocked or empty run can succeed with an empty dataset. Check `emptyReason` in `OUTPUT`, broaden the search or filters, try another country, or enable `RESIDENTIAL`.

#### What should I do if Meta blocks requests?

Enable an Apify `RESIDENTIAL` proxy group in `proxyConfiguration`. Retry behavior is described in Limits and edge cases.

#### Is it legal or allowed to use this public data?

The Actor reads public data shown by the Meta Ad Library without logging in. Source terms and applicable law depend on your use case; review them for your planned use. This Actor does not provide a legal guarantee.

### Usage statistics

This Actor creates a small, content-free summary at the end of each run, used only to monitor reliability and improve this Actor. No input values, credentials or scraped data are sent.

A copy of the exact record for your run is saved as `USAGE_STATS` in your own Apify key-value store.

To opt out, set `disableUsageStats` to `true` in the input. Nothing is sent then.

[Full list of collected fields](https://api.apify.com/v2/key-value-stores/v9rgHjMKLDGtHf8tD/records/index.html)

# Actor input Schema

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

Keywords to search ads for (e.g. 'nike', 'weight loss', 'crypto'). Each term runs as a separate query across every country. Leave empty if you only want advertiser page IDs.

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

Numeric Facebook page IDs to pull ALL ads from a specific advertiser. Find a page ID in the Ad Library: open an advertiser and copy the numeric id from the URL. Works alongside or instead of search terms.

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

Two-letter ISO country codes (e.g. US, GB, DE). The Ad Library requires a country; each is searched separately. Default: US.

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

all = commercial + everything; political_and_issue_ads = only political/issue ads, which additionally carry spend & impressions ranges.

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

Filter ads by whether they are currently running. 'all' returns both active and inactive ads.

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

Filter by creative format. 'all' keeps every format (image, video, carousel, text).

## `matchType` (type: `string`):

broad = ads containing the words in any order; exact = the exact phrase. Applies to search-term queries only.

## `maxResults` (type: `integer`):

Maximum ads to return per search term / page ID per country. The actor paginates automatically. Default 100. Keep low to control cost.

## `pageSize` (type: `integer`):

How many ads to push per batch for real-time output. Lower = faster first results; higher = fewer dataset writes.

## `docId` (type: `string`):

Meta's persisted-query id for the Ad Library pagination query. LEAVE EMPTY: the actor self-heals by re-extracting the live doc_id from each page, so it survives Meta's ~monthly rotation automatically. Only set this to force a specific id (overrides self-healing).

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

Apify Proxy. RECOMMENDED on the cloud: Meta blocks datacenter IPs, so enable a RESIDENTIAL group for token-free scraping at volume. Billed to the run owner — no external proxy account needed.

## `disableUsageStats` (type: `boolean`):

See the README Usage statistics section for details.

## Actor input object example

```json
{
  "searchTerms": [
    "nike",
    "adidas"
  ],
  "countries": [
    "US"
  ],
  "adType": "all",
  "activeStatus": "all",
  "mediaType": "all",
  "matchType": "broad",
  "maxResults": 100,
  "pageSize": 25,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "disableUsageStats": false
}
```

# Actor output Schema

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

Dataset containing Meta Ads Library Scraper records (page_name, is_scaled, collation_count, days_active, is_active, cta_text, ad_archive_id, publisher_platforms, ad_delivery_start_date, link_url, ad_snapshot_url, parse_confidence).

# 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": [
        "nike"
    ],
    "countries": [
        "US"
    ],
    "maxResults": 100,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("bovi/meta-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 = {
    "searchTerms": ["nike"],
    "countries": ["US"],
    "maxResults": 100,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("bovi/meta-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 '{
  "searchTerms": [
    "nike"
  ],
  "countries": [
    "US"
  ],
  "maxResults": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call bovi/meta-ads-library-scraper --silent --output-dataset

```

## MCP server setup

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