# Facebook Ads Library Scraper + AI Ad Creative Analysis (`craftedincode/meta-ads-library-ai-creative`) Actor

Scrape active Facebook and Instagram ads from the public Meta Ad Library by keyword or page, with hard result and cost caps. Optional AI tags hook, angle, offer, CTA, format, persona and a long-runner signal. From $0.70 / 1,000 ads.

- **URL**: https://apify.com/craftedincode/meta-ads-library-ai-creative.md
- **Developed by:** [Crafted in Code](https://apify.com/craftedincode) (community)
- **Categories:** Social media, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 ad scrapeds

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 + AI Ad Creative Analysis

**Pull active Facebook and Instagram ads for a brand or keyword from the public Meta Ad Library, then optionally tag the hook, angle, offer, CTA, format and persona.** `maxAds` and `maxChargeUsd` are hard stops. Empty rows are not billed. From **$0.70 / 1,000 ads** on paid plans, or **$5.70 / 1,000 ads** with text-and-image AI.

Built for performance-marketing agencies, DTC brands, and anyone who uses an ad spy tool to see which competitor creatives have been running the longest. A long-running ad is a common stand-in for a winner. This Actor labels that as a heuristic. Meta does not publish spend or ROAS for ordinary commercial ads, and this Actor does not invent those numbers.

### What data can you extract from the Facebook Ad Library?

| Field | What it is |
| --- | --- |
| `adArchiveId`, `adLibraryUrl` | The public Ad Library id and link |
| `pageId`, `pageName` | The advertiser page |
| `isActive`, `startDate`, `endDate`, `daysRunning` | Delivery window |
| `isLongRunner` | True when the ad has been running at least `longRunnerDays` (default 14). A heuristic, not measured ROAS |
| `bodyText`, `title`, `ctaText`, `linkUrl`, `landingDomain` | The copy and the click destination |
| `mediaType`, `imageUrls`, `videoUrl`, `variantCount` | Still, video, or carousel. Dynamic-creative cards are included. Variants can be deduped |
| `euReach` | Only when Meta exposes EU delivery reach. Otherwise null |
| `hasTemplatePlaceholders` | True when the headline is still a catalogue token such as `{{product.name}}` |
| AI fields | `hookType`, `hookText`, `angle`, `offer`, `ctaIntent`, `emotionalTriggers`, `targetPersona`, `formatTags`, `visualSummary`, `claims`, `complianceFlags`, `aiCreativeScore` |
| Advertiser report | Angle mix, top offers, longest-running ads, creative cadence, short narrative |

Creative files stay on Meta's CDN. The dataset stores URLs and analysis. It does not re-host images or videos.

### How to scrape Facebook ads

1. In **Search terms**, enter a brand or keyword (`nike`, `protein powder`). Or paste an Ad Library URL that contains `view_all_page_id`, or a numeric page id.
2. Leave **Active ads only** on. Inactive ads are dropped before billing.
3. Set **Maximum ads**. The run stops at that count. Set **Maximum charge (USD)** as a second brake, and also set the run's max charge in Apify.
4. Leave **AI creative analysis** on `No AI` for a cheap scrape. Choose **Text and image** or **Include video keyframes** when you want the breakdown.
5. Turn on **Advertiser report** for one page-level summary per brand in the results.
6. Run. Open the **Overview** table for the ads and **AI creative breakdown** for the tags. The key-value record `OUTPUT` says how many ads were scanned, how many were filtered, and why the run stopped.

The prefilled input searches `nike` in the US for 20 active ads, with AI off and a $1 charge cap, so the daily Store test stays small.

You can also pass a **dataset** from another Actor. Rows are read for `brand`, `query`, `searchTerm`, `pageUrl`, `pageId`, or `url`.

### How much does it cost to scrape Facebook ads?

Paid plans (Bronze, Silver, Gold, Platinum, Diamond) use one price. The free plan is higher, because Apify covers platform usage for free users and does not cover the Gemini bill.

| Event | Free plan | Paid plans | When it fires |
| --- | --- | --- | --- |
| `ad-scraped` | $0.0012 | $0.0007 | One non-empty ad saved |
| `ad-ai-analysed` | $0.02 | $0.005 | Text and image breakdown |
| `video-ad-ai-analysed` | $0.05 | $0.015 | Breakdown that used video keyframes. Replaces the text event for that ad |
| `advertiser-report` | $0.50 | $0.15 | One report for one advertiser page |
| `apify-actor-start` | $0.00005 | $0.00005 | Platform start event, default price |

Worked examples on a **paid** plan:

| Run | Price |
| --- | --- |
| 1,000 ads, no AI | **$0.70** |
| 1,000 ads with text-and-image AI | **$5.70** |
| 100 video ads with video analysis | **$1.57** (100 × $0.0007 + 100 × $0.015) |
| 1 advertiser report | **$0.15** |
| 200 ads, 50 of them analysed, 1 report | **$0.54** |

Free plan, same 1,000 ads with AI: 1,000 × $0.0012 + 1,000 × $0.02 = $21.20, and the free plan is also capped (below), so a free user cannot actually buy 1,000 AI rows in one run.

**Free plan limits, stated here and enforced in the run:** 20 AI items per run (analyses and reports share the cap) and 100 AI items per UTC month. Raw ads continue after the cap. The row's `aiSkipReason` says why AI was skipped. The run stops with a normal status, not a crash.

`maxAds` is exact. `maxChargeUsd` stops the run before the next event that would cross it. `Actor.charge` also honors the platform max charge (`ACTOR_MAX_TOTAL_CHARGE_USD`) and stops when `eventChargeLimitReached` is set. Duplicate dynamic-creative variants are not billed when dedupe is on. Rows with no id and no copy or media are not billed.

There is no "pay per event + usage" add-on. Memory is capped at 1 GB so the start event stays a single unit.

#### AI score rubric

`aiCreativeScore` is 0–100:

- Hook clarity, 0–20
- Offer specificity, 0–20
- Persona specificity, 0–20
- CTA strength, 0–15
- Format match, 0–15

Days running are **not** added again inside the score. `isLongRunner` is computed in code from the start date. `aiConfidence` is the model's own 0–1 confidence. Health or financial claims show up in `complianceFlags` only when the copy makes them.

### Input

```json
{
  "searchTerms": ["protein powder"],
  "pageUrls": ["https://www.facebook.com/ads/library/?view_all_page_id=123456789"],
  "country": "US",
  "activeOnly": true,
  "mediaType": "all",
  "startDateFrom": "2026-07-01",
  "maxAds": 200,
  "dedupeVariants": true,
  "aiAnalysis": "text_image",
  "maxAiAds": 200,
  "advertiserReport": false,
  "maxChargeUsd": 5,
  "longRunnerDays": 14,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

`mediaType` and `startDateFrom` are applied **again after download**. Meta accepts those filters and then ignores them. `OUTPUT.stats` includes `filteredDate`, `filteredMedia`, and `scanned`, so you can see that a request for 20 video ads may have read more than 20 rows to find them. `maxScannedAds` (default 20× `maxAds`, never above 20,000) stops that walk.

### Output

A full fixture is in [`examples/sample-output.json`](examples/sample-output.json). One ad looks like this:

```json
{
  "adArchiveId": "1001",
  "pageId": "900001",
  "pageName": "Harbor & Hide",
  "isActive": true,
  "startDate": "2026-08-20",
  "daysRunning": 46,
  "isLongRunner": true,
  "platforms": ["facebook", "instagram"],
  "title": "Still packing lunch in plastic tubs?",
  "bodyText": "The Harbor flask keeps soup hot until 6pm. 20% off the first kit this week.",
  "ctaText": "Shop now",
  "landingDomain": "harborandhide.example",
  "mediaType": "image",
  "variantCount": 2,
  "adLibraryUrl": "https://www.facebook.com/ads/library/?id=1001",
  "hookType": "question",
  "angle": "discount on a daily object",
  "offer": "discount",
  "targetPersona": "person who carries lunch or a drink",
  "formatTags": ["static_product"],
  "aiCreativeScore": 78,
  "aiStatus": "ok"
}
```

Advertiser reports go to the named dataset `advertiser-reports` and include `angleMix`, `topOffers`, `longestRunningAds`, and `creativeCadence`.

### FAQ

#### Is scraping the Facebook Ad Library legal?

The Actor reads the **logged-out** public Ad Library only. It does not log into Facebook, it does not accept account cookies, and it does not collect personal profiles. In Meta Platforms, Inc. v. Bright Data Ltd. (N.D. Cal., January 2024), the court held that logged-off scraping of public data did not breach Meta's terms. Background: [Courthouse News](https://v2.courthousenews.com/federal-judge-rules-against-meta-in-data-scraping-case/) and the [order (PDF)](https://storage.courtlistener.com/recap/gov.uscourts.cand.406956/gov.uscourts.cand.406956.181.0_1.pdf).

That ruling is about public data and logged-out access. It is not a blanket permission to ignore the law in your jurisdiction. Use the data for competitive research you are allowed to do. Ad creatives are copyrighted: this Actor returns URLs and analysis, not a copy of the media files. Business advertiser fields (`pageName`, `pageId`) are included. Personal profile content is not.

The official Ads Library API covers political and social-issue ads, plus some EU transparency fields. It does not return the commercial ads this Actor is for. `euReach` is filled only when Meta includes it.

#### Why do other scrapers return thousands of ads when I asked for 40?

That is the main complaint on the leading Facebook ads Actors: the count limit was ignored, inactive ads were included, and the bill jumped. This Actor stops at `maxAds`, stops at `maxChargeUsd`, drops inactive ads by default, skips empty rows, and dedupes dynamic-creative variants.

#### How is this different from the other Facebook ads scrapers?

| | This Actor | curious_coder/facebook-ads-library-scraper | apify/facebook-ads-scraper | Foreplay |
| --- | --- | --- | --- | --- |
| Paid price | $0.70 / 1,000 ads | about $0.75 / 1,000 | about $5.80 / 1,000 | $59–$459 / month |
| Hard `maxAds` | Yes | Users report overruns | Users report overruns | n/a |
| AI hook, angle, offer, persona | Yes, optional | No | E-commerce enrichment, not creative strategy | Yes, inside the product |
| Long-runner flag | Yes, labeled as a heuristic | Partial | Partial | Yes |

#### Does it use a browser?

No. The Ad Library page itself is often a JavaScript challenge. The ads come from the same logged-out GraphQL call the page uses (`AdLibrarySearchPaginationQuery`), over HTTP with a Chrome TLS fingerprint (`impit`). Cheerio reads the public LSD token from the homepage. Crawlee runs one search at a time so the caps stay exact. Playwright is not installed.

Memory is 256 MB minimum, 512 MB default, 1 GB maximum. Video mode shells out to `ffmpeg` for three JPEG frames (0s, 10s, 20s), scaled to 384 px. That is a short process, not a browser. Frames are sent to the model and discarded. Full video files are not uploaded, because a few megabytes of video through a residential proxy would cost more than the $0.015 event.

If keyframes cannot be read, that ad is billed as `ad-ai-analysed` (the cheaper event) and `videoAnalysis` is `text_only_fallback`.

#### What happens when Meta blocks the request?

An empty HTTP 200 from the GraphQL endpoint is a block, not "no ads found". The Actor retries that page once on residential proxy, up to `maxResidentialRequests` (default 25). If the search still returns nothing because of a block, the run **fails** and charges no ad rows. A genuine empty search (valid JSON, zero ads) succeeds with an empty dataset and says so in `OUTPUT`.

GraphQL document ids rotate. Set `META_AD_LIBRARY_DOC_IDS` (comma-separated) if a future change rejects the built-in ids. The run tries the next id when Meta returns a schema error.

#### Which model does the AI use?

`GEMINI_API_KEY` with model `gemini-3.5-flash-lite` (override with `GEMINI_MODEL` and `GEMINI_REPORT_MODEL`). Temperature is 0 and output is schema-locked JSON. Thinking tokens are billed as output, so they stay off: `gemini-3.5` models omit `thinkingConfig` (that family rejects `thinkingBudget: 0` with a generic HTTP 400), and any other model that rejects the field is retried once without it. If Gemini errors and `OPENAI_API_KEY` is set, the call falls back to `gpt-6-luna` (`OPENAI_MODEL`). Results are cached for 14 days by creative hash so a re-run does not call the model again. The user is still charged for the AI event, because they receive the analysis. The run log warns if estimated LLM spend exceeds 25% of the billed amount.

Set both keys as **secret** environment variables on the Actor. Do not put them in input. The free Gemini tier may use prompts to improve Google's products; use a paid Gemini key for client work.

#### How do I connect it to Make, n8n, Zapier, or an agent?

Run it from the Apify API, a task, or a schedule. Make, n8n, and Zapier all have an Apify integration: map `searchTerms` in and read the default dataset out. The Actor uses limited permissions (its own storages, plus a dataset you explicitly pick) and pay-per-event pricing, which is what Apify's agent payments (x402 / MPP) require. There is no Standby mode.

A website lead qualifier that turns a domain into a company profile is a natural upstream Actor: take the brand name from that dataset and pass it here as `datasetId`.

### Limitations

- Logged-out public Ad Library data only. No account, no cookies, no personal profiles.
- Commercial-ad spend, impressions, and reach are null unless Meta exposes an EU transparency field. Anything else would be made up.
- Date and media filters are re-checked locally. The summary shows how many rows were discarded. `maxScannedAds` bounds that work.
- Ad Library markup and GraphQL doc ids change. Budget time to update `META_AD_LIBRARY_DOC_IDS` when runs start failing with schema errors.
- Residential proxy is required when datacenter exits get an empty 200. The residential cap exists so that cost stays bounded. Measure it on a real 1,000-ad run before you drop the raw price below $0.0007.
- Video analysis is three keyframes, not a spoken transcript. `transcriptFirst30s` stays null.
- AI text can be wrong. Use `aiConfidence`, `complianceFlags`, and the original `bodyText` before you ship a claim.
- This Actor is not affiliated with Meta.

### Local development

```bash
npm install
npm test
npm run e2e
```

`npm run e2e` runs the Actor with `META_ADS_SOURCE=mock` and `LLM_PROVIDER=mock`. It does not call Meta or Gemini. Copy `.env.example` to `.env` for a live run and set `GEMINI_API_KEY`. Publishing steps for the Store are in [PUBLISHING.md](PUBLISHING.md).

# Changelog

This Actor's version history is a separate document: https://apify.com/craftedincode/meta-ads-library-ai-creative/changelog.md

# Actor input Schema

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

Keywords or brand names. Example: protein powder, nike shoes.

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

Ad Library links with view_all_page_id, or a facebook.com page URL. Numeric page ids are searched as that advertiser. A slug without an id is searched as a keyword.

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

Numeric page ids, if you already have them.

## `datasetId` (type: `string`):

Optional dataset from another Actor. Rows are scanned for brand, query, searchTerm, pageUrl, pageId, or url.

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

Two-letter country code of the Ad Library view, or ALL.

## `activeOnly` (type: `boolean`):

When on, inactive ads are dropped and not charged. Default on, because inactive ads are a common cause of surprise bills.

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

Meta often ignores this filter. It is applied again locally, and the run summary shows how many ads were discarded.

## `startDateFrom` (type: `string`):

YYYY-MM-DD. Applied locally. Meta's own date parameter is not reliable.

## `maxAds` (type: `integer`):

Hard stop. The run will not push or charge more ads than this.

## `maxScannedAds` (type: `integer`):

Safety cap on ads read from Meta before local filters, so a strict date or media filter cannot walk the whole library. Default is 20× maxAds, never above 20,000.

## `dedupeVariants` (type: `boolean`):

Keep one row per collation group and set variantCount. Stops dynamic ads from billing the same concept many times.

## `longRunnerDays` (type: `integer`):

Ads still running at least this many days get isLongRunner=true. This is a heuristic, not measured spend or ROAS.

## `maxChargeUsd` (type: `number`):

Hard dollar cap for this run, on top of the max charge you set in the Apify run options. The next event that would cross it is not billed and the run stops.

## `maxResidentialRequests` (type: `integer`):

Residential proxy is only used after a blocked (empty) response. This caps those retries so proxy cost cannot run away.

## `aiAnalysis` (type: `string`):

none: scrape only. text_image: hook, angle, offer, CTA, persona, format, score. full_video: same, and video ads are billed as video-ad-ai-analysed when keyframes can be read.

## `maxAiAds` (type: `integer`):

Cap on AI analyses this run. Free-plan users are also capped at 20 AI items per run and 100 per UTC month, whichever is lower.

## `aiLanguage` (type: `string`):

Language for AI text fields. Ad copy is left as published.

## `advertiserReport` (type: `boolean`):

One report per Facebook page in the results: angle mix, top offers, longest-running ads, cadence. Charged as advertiser-report. Saved to the advertiser-reports dataset.

## `maxAdvertiserReports` (type: `integer`):

Cap on report events.

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

Datacenter proxy first. Residential is a capped fallback after Meta returns an empty 200. Leave Apify proxy on. The Ad Library blocks plain datacenter exits.

## Actor input object example

```json
{
  "searchTerms": [
    "nike"
  ],
  "country": "US",
  "activeOnly": true,
  "mediaType": "all",
  "maxAds": 20,
  "dedupeVariants": true,
  "longRunnerDays": 14,
  "maxChargeUsd": 1,
  "maxResidentialRequests": 25,
  "aiAnalysis": "none",
  "maxAiAds": 20,
  "aiLanguage": "en",
  "advertiserReport": false,
  "maxAdvertiserReports": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Ads scanned, delivered, filtered, charged, and the stop reason (max ads, max charge, scan cap, or blocked).

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

One row per billed 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 = {
    "searchTerms": [
        "nike"
    ],
    "country": "US",
    "activeOnly": true,
    "mediaType": "all",
    "maxAds": 20,
    "dedupeVariants": true,
    "longRunnerDays": 14,
    "maxChargeUsd": 1,
    "aiAnalysis": "none",
    "maxAiAds": 20,
    "advertiserReport": false,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("craftedincode/meta-ads-library-ai-creative").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"],
    "country": "US",
    "activeOnly": True,
    "mediaType": "all",
    "maxAds": 20,
    "dedupeVariants": True,
    "longRunnerDays": 14,
    "maxChargeUsd": 1,
    "aiAnalysis": "none",
    "maxAiAds": 20,
    "advertiserReport": False,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("craftedincode/meta-ads-library-ai-creative").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"
  ],
  "country": "US",
  "activeOnly": true,
  "mediaType": "all",
  "maxAds": 20,
  "dedupeVariants": true,
  "longRunnerDays": 14,
  "maxChargeUsd": 1,
  "aiAnalysis": "none",
  "maxAiAds": 20,
  "advertiserReport": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call craftedincode/meta-ads-library-ai-creative --silent --output-dataset

```

## MCP server setup

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

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/U7zMF6A8AXbGUBbeQ/builds/JeTGWxdJMDZ0NrTbu/openapi.json
