Facebook Ads Library Scraper + AI Ad Creative Analysis
Pricing
from $0.70 / 1,000 ad scrapeds
Facebook Ads Library Scraper + AI Ad Creative Analysis
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.
Pricing
from $0.70 / 1,000 ad scrapeds
Rating
0.0
(0)
Developer
Crafted in Code
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
18 days ago
Last modified
Categories
Share
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
- In Search terms, enter a brand or keyword (
nike,protein powder). Or paste an Ad Library URL that containsview_all_page_id, or a numeric page id. - Leave Active ads only on. Inactive ads are dropped before billing.
- 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.
- Leave AI creative analysis on
No AIfor a cheap scrape. Choose Text and image or Include video keyframes when you want the breakdown. - Turn on Advertiser report for one page-level summary per brand in the results.
- Run. Open the Overview table for the ads and AI creative breakdown for the tags. The key-value record
OUTPUTsays 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
{"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. One ad looks like this:
{"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 and the order (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.
maxScannedAdsbounds that work. - Ad Library markup and GraphQL doc ids change. Budget time to update
META_AD_LIBRARY_DOC_IDSwhen 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.
transcriptFirst30sstays null. - AI text can be wrong. Use
aiConfidence,complianceFlags, and the originalbodyTextbefore you ship a claim. - This Actor is not affiliated with Meta.
Local development
npm installnpm testnpm 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.