# Meta Ad Library Scraper — Resume, Media & $0-on-Block (`domestic_buffalograss/meta-ad-library-reliable`) Actor

Search the Meta Ad Library by keyword and country. Returns ad copy, landing URL, CTA type, days running, placement mix, the country each ad was found in, and creative image/video URLs. Never charged for blocked runs.

- **URL**: https://apify.com/domestic\_buffalograss/meta-ad-library-reliable.md
- **Developed by:** [Sumitsubo](https://apify.com/domestic_buffalograss) (community)
- **Categories:** Marketing, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 ad scraped (1 result)s

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Meta Ad Library Scraper — Resume, Media & $0-on-Block

Fetch ads from the Meta (Facebook/Instagram) Ad Library **without the two things that make every other scraper frustrating**: silent data loss and getting charged when the run was actually blocked.

### Why this one

Most Ad Library scrapers crash on large runs, return incomplete data, and can't resume — and some charge you even when Meta blocked the request. This Actor is built reliability-first:

- **Resume** — a killed or timed-out run continues exactly where it stopped. No re-fetching, no double charges.
- **No silent loss** — every work unit is checkpointed; failures are written to the dataset as `{_error, _reason}` records, never dropped.
- **$0 when blocked** — a blocked/rate-limited request is detected and **not billed**. You pay only for ads actually delivered.
- **Real media files** — optionally download creative videos/images to key-value storage (not just snapshot URLs).
- **Clean, stable schema** — flat, null-safe, `snake_case`, with `duration_days` and `_meta` on every row so it drops straight into your pipeline.

### What it does NOT do (honest limits)

Meta does **not** expose spend, impressions, reach, CTR, ROAS, or targeting for commercial ads (only political/issue ads, or EU DSA reach). **This Actor does not invent those numbers.** If you need spend/impressions, no scraper can get them for commercial ads — don't trust any that claim to.

### Input

| Field | Meaning |
|---|---|
| `mode` | `snapshot` (ads only) or `snapshot_with_media` (also download creative files) |
| `advertisers` | Keywords to search the Ad Library for (required). This is a **keyword search, not an exact advertiser filter** — `Nike` also returns retailers and resellers whose ads mention Nike. Page IDs are not supported. |
| `countries` | ISO country codes (Ad Library is per-country; default `US`) |
| `maxItems` | Hard cap on ads — you are never charged beyond this |
| `maxBudgetUsd` | Optional hard USD ceiling; the run stops before exceeding it |

### Output (one row per ad)

```json
{
  "ad_archive_id": "123", "country": "US", "page_name": "Nike", "page_id": "p1",
  "ad_delivery_start": "2026-07-01", "ad_delivery_stop": "2026-07-15",
  "duration_days": 14, "is_active": false,
  "publisher_platforms": ["facebook", "instagram"],
  "ad_creative_bodies": ["..."],
  "collation_count": 2,
  "media": [{ "kind": "video", "url": "...", "stored_key": "media_video_ab12.mp4" }],
  "image_urls": ["https://scontent.xx.fbcdn.net/..."],
  "video_urls": ["https://video.xx.fbcdn.net/..."],
  "landing_url": "https://...", "cta_type": "SHOP_NOW",
  "_meta": { "_scraped_at": "2026-09-02T00:00:00Z", "_source_url": "...", "_actor_version": "<actor build version>", "_schema_version": "<schema version>" }
}
```

- `country` — the country library this ad was found in. The Ad Library is per-country, so this is what lets you split a multi-country run by market.
- `media` — creative files that will be **downloaded to key-value storage** in `snapshot_with_media` mode. `stored_key` is filled in once the file is saved, so every file you are charged for can be looked up in the run's key-value store. Restricted to `fbcdn.net` hosts and capped per ad, so a malicious advertiser host can't turn your dataset into a tracking beacon and per-ad media cost can't blow up.
- `image_urls` / `video_urls` — **all** creative URLs found (full resolution), for reference. These are never downloaded and never billed.

Blocked/failed units appear as `{ "_error": true, "_reason": "blocked", "_source_url": "...", "_work_unit": "..." }`.

### Pricing (pay-per-event)

| Event | Price |
|---|---|
| `actor-start` | $0.01 (first runs previewable free) |
| `ad-scraped` | $0.001 per ad |
| `media-downloaded` | $0.007 per file (only in `snapshot_with_media`) |

You are **not** charged for blocked runs or for ads beyond `maxItems` / `maxBudgetUsd`.

### Development status

**Live and ready.** The reliability core is complete and adversarially reviewed, and the live Meta fetch is wired against Meta's internal Ad Library GraphQL endpoint (`AdLibrarySearchPaginationQuery`), captured from a real browser session and pinned as an offline fixture (`tests/fixtures/`). `npm test` runs the full suite (reliability core + live-fetch parsing/pagination/blocked/429/5xx paths + billing-name pins) with **no network access**.

Fault-injection coverage proves the money-safety and kill-safety invariants: mid-run kill → no double charge & no data loss; blocked/rate-limited/empty → $0 charge; source exception → error record (no crash); push failure → not billed; media download failure (non-2xx) → not billed; budget/`maxItems` ceilings enforced and **persisted across resumes** (integer micro-dollar accounting, no float drift); `maxBudgetUsd=0` charges nothing. Invalid input / missing residential proxy → **ABORTED, not FAILED** (protects Store Quality Score); charge event names are pinned in `tests/billing.test.js` and reconciled with Console pricing at startup (`SUMMARY.monetization`).

#### How the live fetch works

The fetch is a **hybrid**: bootstrap (getting the `lsd` token and `doc_id` from the public Ad Library page) runs in a **real headless Chrome** (`src/sources/browserBootstrap.js`, patchright) over a residential proxy with a **disposable session per attempt** (Meta's JS anti-bot challenge blocks plain HTTP clients on this page — measured 6/6 in production; bad shared-pool IPs are handled by rotating sessions with up to 4 retries). The GraphQL paging itself then runs over lightweight HTTP (`got-scraping`) — no login, no cookies (the GraphQL endpoint accepts requests with only the `lsd` token, verified in production). If a page unexpectedly comes back empty (low-reputation IP degradation), the actor **discards both the session and the paging client and retries with fresh IPs** up to 2 times before accepting a genuine empty result. Logged-out only — this keeps it inside the *Meta v. Bright Data* (N.D. Cal. 2024) safe harbor for logged-out public data.

**If Meta rotates the `doc_id`** and results stop coming back: open `https://www.facebook.com/ads/library/?q=nike&country=US&active_status=active&ad_type=all` in a browser, find `AdLibrarySearchPaginationQuery_facebookRelayOperation` in the JS bundle, and update `DOC_ID_FALLBACK` in `metaAdLibrary.js`. The code already tries to extract it dynamically first, so this is only a safety net.

### Architecture

Platform-independent core (`src/core`, `src/orchestrator.js`) is fully unit-tested without the Apify platform. `src/platform/apify.js` and `src/sources/metaAdLibrary.js` are thin adapters. This is the shared core reused across the ad-library family (Google/Meta/LinkedIn) with one output-schema contract.

# Actor input Schema

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

What to do. 'snapshot' fetches ads once. 'snapshot\_with\_media' also downloads creative files.

## `advertisers` (type: `array`):

Keywords to search the Meta Ad Library for. This is a keyword search, not an exact advertiser filter: "Nike" also returns retailers and resellers whose ads mention Nike. Page IDs are not supported.

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

Two-letter country codes. Ad Library is per-country.

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

Hard cap on billable ads. You are never charged for more than this many ads across the whole run (including resumes).

## `maxBudgetUsd` (type: `number`):

Optional hard USD ceiling (supports decimals, e.g. 0.5). The run stops before exceeding it, and the ceiling persists across resumes. Leave empty for no cap; 0 means charge nothing.

## Actor input object example

```json
{
  "mode": "snapshot",
  "advertisers": [
    "Nike",
    "Coca-Cola"
  ],
  "countries": [
    "US"
  ],
  "maxItems": 500
}
```

# Actor output Schema

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

Every ad this run collected, one row per ad. Never charged for blocked/failed work units.

## `archivedMedia` (type: `string`):

Creative videos/images downloaded to the key-value store. Present only when mode is snapshot\_with\_media.

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

What the run did: how many ads were fetched, blocked/empty units, and whether billing was active and matched the code's expected prices.

# 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": "snapshot",
    "advertisers": [
        "Nike"
    ],
    "countries": [
        "US"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("domestic_buffalograss/meta-ad-library-reliable").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": "snapshot",
    "advertisers": ["Nike"],
    "countries": ["US"],
}

# Run the Actor and wait for it to finish
run = client.actor("domestic_buffalograss/meta-ad-library-reliable").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": "snapshot",
  "advertisers": [
    "Nike"
  ],
  "countries": [
    "US"
  ]
}' |
apify call domestic_buffalograss/meta-ad-library-reliable --silent --output-dataset

```

## MCP server setup

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

```

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/zctkgGIZAeea7zBV4/builds/ITFt44GufweLL73VR/openapi.json
