# Meta Ad Library (`lanternlab318/meta-ad-library`) Actor

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

## Pricing

$1.49 / 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

## Meta Ad Library Scraper (Facebook & Instagram Ads)

Search the public Meta Ad Library by keyword or by advertiser Page and get every ad back as a row: the Library ID and link, the advertiser Page, whether it is still running, start and end date, the platforms it ran on (Facebook, Instagram, Messenger, Audience Network, Threads), the ad text, headline, link, button text, and the image and video URLs. For political and issue ads you also get the spend range, impressions range, currency and the "Paid for by" line. No Facebook login, no login cookies, no account of your own. **$1.49 per 1,000 ads, no start fee.** See [Pricing](#pricing).

Input:

```json
{
  "searchTerms": ["running shoes", "election"],
  "pageUrls": ["https://www.facebook.com/nike"],
  "country": "US",
  "adType": "all",
  "activeStatus": "active",
  "mediaType": "all",
  "maxItems": 200
}
```

Output - one row per ad (`recordType: "ad"`); per-target status rows and the run summary are free. Sample from a real run (video URL shortened):

```json
{
  "recordType": "ad",
  "adArchiveId": "1134693002421653",
  "url": "https://www.facebook.com/ads/library/?id=1134693002421653",
  "pageId": "108107432370909",
  "pageName": "Kalshi - Trade the Headlines",
  "pageUrl": "https://www.facebook.com/KalshiTradeTheHeadlines/",
  "paidForBy": "KALSHI INC.",
  "isActive": true,
  "startDate": "2026-10-11",
  "endDate": null,
  "platforms": ["FACEBOOK", "INSTAGRAM"],
  "countries": [],
  "searchCountry": "US",
  "displayFormat": "VIDEO",
  "bodyText": "Who will win the House? Trade it on Kalshi.",
  "title": "Trade what's next",
  "linkUrl": "http://itunes.apple.com/app/id1632713844",
  "ctaText": "Install now",
  "imageUrls": [],
  "videoUrls": ["https://video.xx.fbcdn.net/o1/v/t2/f2/m366/AQP9wr8P….mp4?…"],
  "collationCount": 2,
  "impressionsRange": ">1M",
  "spendRange": "$700K - $800K",
  "currency": "USD",
  "searchTerm": "election",
  "rank": 3,
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-24T09:55:03.118Z"
}
```

Limits:

- **Public data only.** What the Ad Library shows a logged-out visitor: no targeting breakdowns (age, gender, region), no EU reach report, no ad-level demographic data. Spend, impressions and currency exist only for political and issue ads; for every other ad they are `null`.
- **Image and video URLs are Meta's signed CDN links and expire after a few days.** Download what you need soon after the run.
- **Every search term and Page gets a status row saying what happened.** A search that found nothing is an empty result; one cut short by a limit, a refusal or an error is `partial` with the reason - the two are never confused.
- **Your limits stop collection and appear in the row**: `maxItems`, the `maxUpstreamRequests` request ceiling and the maximum charge per run (see [Pricing](#pricing)).

### What it does

- Searches each keyword and each Page separately and labels every row with the target that produced it (`searchTerm` or `searchPageId`), so one run can cover many brands or topics.
- Accepts Pages as a Facebook Page URL, a numeric Page ID, or an Ad Library link with `view_all_page_id`. For a Page URL the numeric Page ID is read from the Page's public page first.
- Filters the way the Ad Library does: country (or all countries), ad category (all ads, or issues, elections and politics), active / inactive / both, media type, and exact-phrase matching.
- Deduplicates across the whole run: an ad found by several of your search terms or Pages is written once and charged once.
- Keeps the Ad Library's own identifier (`adArchiveId`), its start date as the calendar day the Ad Library page shows (`startDate`, plus `publishedAt` as an exact instant) and our collection time (`collectedAt`) as separate fields. `endDate` is `null` while an ad is still running.
- Says what happened per target. A search the Ad Library answered with nothing is recorded as an empty result; one that was cut short is marked `partial: true` with the reason.
- Caps itself: `maxUpstreamRequests` is a hard ceiling on requests to the Ad Library for the whole run, derived from your input when you leave it empty, so a run is never unbounded.
- Charges only ad rows. Empty results, per-target status rows and the run summary are free.

### What it does not do

- No login, no account and no session cookies of your own: it reads only what the public Ad Library shows any visitor.
- No targeting or audience data, no EU transparency report, no advertiser contact details.
- It does not download images or videos; it returns their URLs.
- No scheduling or monitoring: one run, one snapshot. Schedule the Actor on Apify if you need a recurring snapshot.

### Input

| Field | Default | What it does |
| --- | --- | --- |
| `searchTerms` | – | Keywords or phrases, searched like the Ad Library search box. |
| `pageUrls` | – | Facebook Page URLs, numeric Page IDs, or Ad Library links with `view_all_page_id`. Give search terms, Pages, or both. |
| `country` | `ALL` | `ALL`, or a two-letter country code such as `US`, `GB`, `DE`, `BR`, `JP`. |
| `adType` | `all` | `all`, or `political_and_issue_ads` (the only category with spend, impressions and "Paid for by"). |
| `activeStatus` | `active` | `active`, `inactive` or `all`. |
| `mediaType` | `all` | `all`, `image`, `video`, `meme`, `image_and_meme`, `none`. |
| `exactPhrase` | `false` | Match search terms as an exact phrase. |
| `maxItems` | `100` | Most ads one search term or Page may produce (up to 5,000). |
| `maxUpstreamRequests` | derived | Hard ceiling on requests to the Ad Library for the whole run. |
| `proxyConfiguration` | Apify residential | The Ad Library refuses to page results for datacenter IPs, so residential is the default and what the Actor is measured on. |

### Output

Three kinds of row, told apart by `recordType`:

- `ad` - one per ad, the only charged row. Fields as in the sample above, plus `pageCategories`, `pageLikeCount`, `categories`, `linkDescription`, `caption`, `ctaType`, `videoPreviewUrls`, `cardCount` (carousel cards), `collationId` and `reachEstimate`.
- `target_status` - one per search term or Page: `adsReturned`, `adsCharged`, `duplicatesSkipped`, `upstreamRequests`, `partial`, `partialReason` and a plain-language `statusText`.
- `RUN_SUMMARY` - the last row, also saved as `RUN_SUMMARY` in the run's key-value store: totals, empty and partial targets by reason, requests sent and why collection stopped.

#### Partial is not empty

`partial: true` means a target's results are incomplete, never that it has no ads. `partialReason` says why:

| Reason | Meaning |
| --- | --- |
| `upstream_request_cap` | The run reached its `maxUpstreamRequests` ceiling while on this target. |
| `charge_limit_reached` | The maximum charge you set for the run was reached. |
| `blocked` | The Ad Library refused our requests (rate limit or bot check) even after switching to a new proxy IP. |
| `upstream_error` | A request failed in a way retrying inside the run would not fix. |
| `page_not_resolved` | A Page URL whose numeric Page ID could not be read; pass the numeric ID instead. |
| `not_collected` | The run stopped earlier for one of the reasons above and never reached this target. |

A run that collected nothing because every target was refused ends as failed, not as an empty success.

### Limits and boundaries

- `countries` lists the countries the Ad Library publishes for an ad, which it does for only some ads (mostly EU-delivered and political ones); otherwise it is empty and `searchCountry` tells you which country filter found the ad.
- The Ad Library returns ads 10 at a time in its own relevance order; `rank` is the position in that order for the target.
- Ads that share one creative are grouped by the Ad Library (`collationCount`); each ad in a group is its own row.
- Dynamic-creative ads store template text such as `{{product.name}}`; `bodyText`, `title` and `linkDescription` then carry the first card's text, which is what the Ad Library page shows. Catalog ads whose cards hold template text too keep the template, as the Ad Library itself does (about 2% of rows in our test runs). Tracking placeholders inside `linkUrl` are kept as the Ad Library lists them.
- A browser is started once per run and reused for every target, so the first target of a run takes a few seconds longer.

### Pricing

Pay per event, no start fee:

| Event | Price | Charged for |
| --- | --- | --- |
| `result` | $0.00149 ($1.49 per 1,000) | Each ad row written. Per-target status rows, empty results and the run summary are free. |

Set a maximum charge for the run and the Actor stops before going over it.

# Actor input Schema

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

One keyword or phrase per line, searched the way the Ad Library search box does it: ads whose text contains the words. Every term is searched separately and its rows are labelled with it. Give search terms, Pages, or both.

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

Advertisers whose ads you want, one per line: a Facebook Page URL (https://www.facebook.com/nike), a numeric Page ID (15087023444), or an Ad Library link that already has view_all_page_id in it. For a Page URL the numeric Page ID is read from the Page's public page first.

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

Where the ads were shown: "ALL" for every country, or a two-letter country code such as "US", "GB", "DE", "BR", "JP".

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

"all" searches every ad. "political_and_issue_ads" searches ads about social issues, elections or politics - the only category for which the Ad Library publishes spend, impressions and the "Paid for by" line.

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

Only ads still running, only ads that have stopped, or both.

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

Only ads with this kind of creative.

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

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

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

Upper bound on the ads one search term or Page may produce. The Ad Library pages 10 ads at a time.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the requests this run sends to the Ad Library: browser sessions, result pages, Page lookups and retries together. At the ceiling collection stops - the targets not reached are written with partialReason "upstream_request_cap" and the run then ends normally. Leave empty and the Actor works a ceiling out from your targets and limits, so a run is never unbounded.

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

The proxy the Ad Library is read through. The Ad Library refuses to page results for datacenter IPs, so the default is Apify residential proxy, which is what this Actor is measured on.

## Actor input object example

```json
{
  "searchTerms": [
    "running shoes"
  ],
  "country": "ALL",
  "adType": "all",
  "activeStatus": "active",
  "mediaType": "all",
  "exactPhrase": false,
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (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 = {
    "searchTerms": [
        "running shoes"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/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 = { "searchTerms": ["running shoes"] }

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/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 '{
  "searchTerms": [
    "running shoes"
  ]
}' |
apify call lanternlab318/meta-ad-library --silent --output-dataset

```

## MCP server setup

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