Facebook Ads Library Scraper — Meta Ad Library & Ad Spy
Pricing
from $0.60 / 1,000 ad scrapeds
Facebook Ads Library Scraper — Meta Ad Library & Ad Spy
Facebook Ads Library & Meta Ad Library scraper (Facebook Ad Library, Meta ads, Instagram ads): search by keyword, advertiser, page or URL, spy on competitor ads, find winning ads (score, reach/day, days running), products & prices behind them, plus a new & stopped ads monitor. $0.60/1,000 ads.
Pricing
from $0.60 / 1,000 ad scrapeds
Rating
0.0
(0)
Developer
Mr Zack
Maintained by CommunityActor stats
0
Bookmarked
13
Total users
9
Monthly active users
5 days ago
Last modified
Categories
Share
Facebook Ad Library Scraper — Meta Ads, Winners & Monitor
Scrape the Meta Ad Library (Facebook, Instagram, Messenger, Threads, Audience Network) by keyword, by advertiser name (just type "gymshark" or paste facebook.com/gymshark — no page-ID hunting), by page ID, by single ad ID, or by pasting any Ad Library URL. Every ad comes back as one flat row: ad copy, headline, CTA, landing page, images, videos, carousel cards, start date, days running, versions, rank in Meta's impressions order, reach per day (EU/UK), whether the advertiser is scaling that message (same copy in 3+ ads) and a transparent winning score.
Turn on includeLandingPage and every ad also shows the product, price, compare-at price & discount, stock status, rating and store platform (Shopify, WooCommerce, Wix, ClickFunnels …) behind its link — included in the price.
Meta's image and video links expire after a few days. Turn on saveCreatives and each ad's images (and with saveVideos its videos) are saved into your run's storage so your swipe file keeps working — $0.003 per file (per started 5 MB), only for files that were actually saved.
Run it once for research, or on a schedule as a watchlist: new ads are flagged, ads that were switched off are listed, and Meta's ad count per advertiser is compared with the previous run — all free on top of the ads you receive.
Every ad also comes with what Meta publishes under "See ad details" — included in the price: reach (EU/UK) broken down by country, age and gender, targeting (age, gender, locations), payer & beneficiary, and the advertiser's Instagram handle, followers and verification.
$0.60 per 1,000 ads, details included. Duplicates, filtered-out ads and ads you already received in monitor mode are never charged. No login, no cookies, no API key, no browser. Deep paging runs through Apify residential proxy automatically (≈ $0.035 of proxy traffic per 1,000 ads on your Apify bill — measured: 450 ads with details in 88 s for $0.016 total platform usage).
Why this one
| This Actor | |
|---|---|
| Find an advertiser by name / @handle / page URL | ✅ resolved to the page ID automatically (exact match first, then verified / most-liked, alternatives in RUN_SUMMARY) |
| Winning-ad signals | ✅ reachPerDay (EU/UK), isScaled / copyVariantsInRun (same message in several ads), daysActive, versionsCount, rankInQuery and a 0–100 winningScore that combines them — computed after the reach data is in |
| Watchlists | ✅ watchlistId: per-client memory, newSinceLastRun on every ad, ads stopped since the last run and Meta's ad count now vs last run (queryAdsTotal / queryAdsTotalPrev) — free |
| Schedules | ✅ startedWithinDays (e.g. "launched in the last 7 days", relative to each run) — filtered ads are free |
| Brand protection | ✅ excludeAdvertisers drops your own pages (free); copyAdvertisersInRun shows when several pages run the same copy |
| Products & prices behind the ad | ✅ includeLandingPage: product, brand, price, compare-at price & discount %, currency, stock, rating and the store platform, read from the landing page's public product data — included in the price, off by default |
| Budget cap | ✅ maxAdsTotal: one cap on ads (= charged rows) for the whole run, across all searches, pages and links |
| Switching tools? | ✅ inputs written for other Ad Library scrapers (startUrls / urls, resultsLimit, count, period, scrapeAdDetails, onlyAdsNewerThan …) are understood — swap the Actor ID in n8n / Make / your API call and keep your JSON |
| Sort | ✅ impressions (Meta's default) or newest first for monitoring |
| EU/UK audience data | ✅ includeAdDetails (on by default, no extra charge): reach by country/age/gender, targeting, payer/beneficiary |
| Flat output | ✅ one clean row per ad, same columns every time — opens straight in Sheets/Excel, no nested snapshot to unpack |
| Deep paging | ✅ automatic: page 1 direct, deeper pages via residential proxy with IP rotation; parallel queries |
| Monitor mode | ✅ onlyNewAds — pay only for ads you haven't seen |
| Swipe files | ✅ creativeKey groups identical creatives; mediaExpiresAt tells you when Meta's image/video links stop working |
| Creative archive | ✅ saveCreatives (+ saveVideos): the ad's image / video files saved to the run's key-value store, links that do not expire — $0.003 per file per started 5 MB, failed files free (once enabled — see below) |
| AI daily digest | ✅ aiDigest: schedule it on your competitors and get one "what changed today" row per competitor — new angles, offers, hooks, notable new ads, what stopped, 3 takeaways — $0.05 per digest, quiet days free (coming early December 2026 — see below) |
| AI ad analysis | ✅ includeAiAnalysis: hook, persuasion angle, offer, audience, pain points, benefits, CTA intent, tone and sentiment per ad — $0.005 per analysed ad, half the usual $0.01 (coming early December 2026 — see below) |
| Lead lists | ✅ free ADVERTISERS CSV: one row per advertiser with page, likes, Instagram, ad count, landing domains |
| Dynamic/catalog ads | ✅ {{product.name}} placeholders replaced by the real card copy |
| Honest empties | ✅ an empty result page is re-checked with a fresh session before we report "0 ads" |
| Ad counts | ✅ countOnly: one row per search × country with Meta's ad count — 0s included, many countries per run, trend with a watchlist ($0.004 per count, once enabled — see below) |
| Price | $0.60 / 1,000 ads, details included |
Who this is for
- Media buyers & performance marketers — find the ads that are actually working: high reach per day, a message the advertiser runs in several ads (
isScaled), and a long run. Sort bywinningScore, filter withminDaysActive. - E-commerce & dropshipping teams — see which products competitors push, with the exact landing pages (
landingDomain,linkUrl) and creatives; withincludeLandingPagealso the product, price, discount and whether it is a Shopify / WooCommerce store — next to how long the ad has run and itswinningScore. - Agencies — pull a prospect's full ad footprint by page ID for a pitch deck, or schedule a weekly watchlist per client: what is new, what was switched off, and whether the competitor is ramping up (ad count now vs last week).
- Brand protection — search your brand name, exclude your own pages, and see who else advertises with it, where the traffic goes (
landingDomain) and whether they copy your ad text. - Lead-gen agencies — find every advertiser active in a niche and country; the free
ADVERTISERSCSV is one row per advertiser. - Political & issue researchers — spend and impressions ranges as numbers, "paid for by", UK/EU reach by age and gender, and the page's total political spend.
- Creative strategists & copywriters — build swipe files of hooks, headlines and CTAs by niche keyword.
- AI agents (MCP) — "what ads is brand X running on Instagram right now, and which have run the longest?" is one tool call with a predictable price.
What you get per ad
| Field | Example | Notes |
|---|---|---|
adArchiveId, libraryUrl | 925321173274919 | Open the ad in Meta's library |
pageName, pageId, pageUrl, pageLikes, pageCategories | Nike, 15087023444 | The advertiser |
isActive, startDate, endDate, lastSeenDate | true, 2026-08-03 | Run window |
daysActive | 53 | Days the ad has been running (to today, or to its end date) |
isNew | false | Started within newWithinDays (default 7) |
versionsCount | 4 | How many near-identical versions Meta grouped together — a scaling signal |
platforms | facebook, instagram, threads | Where it is shown |
displayFormat | video / image / carousel / dpa / dco | Creative type (dpa/dco = catalog/dynamic ads) |
bodyText, title, linkDescription, caption | Ad copy (dynamic {{product.name}} placeholders are replaced by the real card copy) | |
ctaText, ctaType | Shop Now, SHOP_NOW | Call to action |
linkUrl, landingDomain | https://www.footlocker.com/…, footlocker.com | Destination (Meta's l.facebook.com redirect is unwrapped) |
primaryImageUrl, primaryVideoUrl, imageUrls, videos | Creative media (HD + SD video, preview image) | |
cards, cardCount | Carousel / catalog cards: title, body, link, image, video | |
spend, impressions, currency, disclaimer | Filled for political & issue ads (Meta publishes these only there). spend = {lower, upper, text}, e.g. {40000, 45000, "£40K-£45K"} | |
impressionsRange | {1000000, null, ">1M"} | Political & issue ads: Meta's impressions bucket as numbers |
reachEstimate | Political & issue ads: Meta's reach bucket, e.g. >1M | |
containsAiGeneratedMedia | false | Meta's AI-media label |
rankInQuery | 3 | Position in Meta's result order (impressions high → low by default) |
winningScore | 78 | 0–100, see "How the winning score works" below |
reachPerDay | 12365 | reachTotal ÷ days running — the ad's daily audience (EU/UK, with ad details) |
reachMalePct | 65 | Share of men in the ad's EU/UK reach (unknown gender left out) |
isScaled, copyVariantsInRun | true, 7 | The advertiser runs this copy + headline in 3+ ads (any image/video) of this query's results |
creativeKey, creativeCopiesInRun | 9b466214a010227f, 1 | Identical creative (same copy, headline and image/video) and how many of the advertiser's ads use it |
copyAdvertisersInRun | 1 | How many different advertisers in this query's results run the same copy (2+ = shared or copied text) |
mediaExpiresAt | 2026-10-22T13:24:15.000Z | When Meta's signed image/video links of this row stop working — download creatives before this |
queryAdsTotal, queryAdsTotalPrev | 284, null (first run) | Meta's total for the query with your filters (for an advertiser: its ad count), and the same number from the previous run of the watchlist / monitor |
newSinceLastRun | true | With a watchlist or monitor mode: new since the previous run (null on the first run) |
reachTotal, reachRegion, reachByCountry, reachByAgeGender | 14565, EU, {"DE":14235} | With includeAdDetails, for ads shown in the EU/UK |
targetingAge, targetingGender, targetingLocations | 18-65, All, Germany | With includeAdDetails |
payer, beneficiary | ALLBIRDS, INC. | With includeAdDetails (EU DSA disclosure) |
advertiserIgUsername, advertiserIgFollowers, advertiserVerification, advertiserAbout | allbirds, 511773, BLUE_VERIFIED | Free for advertisers inputs; for every ad with includeAdDetails |
advertiserPoliticalSpend | {lifetime, weekly, lifetimeByDisclaimer, weeklyByDisclaimer} | Political pages, with includeAdDetails: the page's political-ad spend as Meta shows it (Meta gives no currency with these figures) |
searchQuery, searchCountry, scrapedAt | Which of your inputs produced the row | |
landingStatus, landingFinalUrl, landingPlatform, landingPageTitle | ok, https://uk.huel.com/products/black-edition, shopify | With includeLandingPage: what the landing page answered, where the link ends after redirects, the store / site platform |
landingProductName, landingBrand, landingPrice, landingCompareAtPrice, landingDiscountPct, landingCurrency, landingAvailability, landingRating, landingReviewCount | Black Edition, Huel, 32.5, 40, 19, GBP, InStock, 4.6, 12873 | With includeLandingPage: the product behind the ad, from the page's public product data (schema.org / Shopify) |
savedCreatives, creativesSavedCount | [{"type":"image","key":"ad_925321173274919_1.jpg","url":"https://api.apify.com/v2/key-value-stores/…/records/ad_925321173274919_1.jpg","bytes":184211,"contentType":"image/jpeg"}], 1 | With saveCreatives: the ad's files saved in this run's key-value store (type = image, video-thumbnail or video) |
creativeErrors | [{"type":"image","sourceUrl":"https://scontent…","reason":"blocked"}] | With saveCreatives: files that were not saved — and not charged — with the reason |
aiAnalysis | {"status":"ok","hook":"Lunch in 30 seconds","angle":"feature-benefit","offer":"15% off first order","offerType":"discount","audience":"busy professionals",…} | With includeAiAnalysis: the AI read of the ad (see "AI ad analysis") |
Every field of an ad row is always present — null when Meta does not publish it, never a made-up value (the landing… fields are added only with includeLandingPage, the creative-file fields only with saveCreatives, aiAnalysis only with includeAiAnalysis). (Only want the number of ads? See "Ad counts mode" below.)
A free RUN_SUMMARY record (key-value store) adds per query: Meta's reported total (and last run's, with a watchlist), ads delivered, pages fetched, why a query stopped, the most-used messages (topCopies) and — for watchlists — the ads stopped since the last run; and per advertiser: page likes and Instagram, ads, active / new / scaled ads, the advertiser's ad count in the library (advertiser and page queries), format, CTA and platform mix, top landing domains, oldest start date and longest-running ad. The same advertiser list is saved as a free ADVERTISERS CSV (one row per advertiser — open it in Sheets or import it into a CRM).
How the winning score works
winningScore (0–100) is computed per query, after the ad details are merged: reach per day percentile among the query's ads (30), days running (25, full at 90 days), same message in several ads (15: 2 / 3 / 4+ ads), Meta's versions (10, full at 5), impressions rank (10, only with sortBy impressions), placements (5) and still active (5). When Meta publishes no reach (outside the EU/UK) or there is no impressions rank, those parts are left out and the rest is rescaled to 0–100, so a US ad is not punished for missing EU data. Rows are never merged or dropped for this — every ad stays one row.
Input
| Field | Default | What it does |
|---|---|---|
searchTerms | — (the form pre-fills nike as an example) | Keywords / brands searched in all ad copy. Ad Library URLs pasted here work too |
advertisers | [] | Every ad of an advertiser by name, @handle, facebook.com/… or instagram.com/… URL |
pageIds | [] | Same, by numeric page ID (view_all_page_id=…) — pins the exact page |
adIds | [] | Specific ads by Ad Library ID or ?id= link |
adLibraryUrls | [] | Paste a search URL from facebook.com/ads/library — all its filters are applied |
excludeAdvertisers | [] | Skip ads of these advertisers (exact page name, page ID or facebook.com/… URL) — skipped ads are free |
country | ALL | ISO-2 country (US, GB, DE, BR, ID…) or ALL |
activeStatus | active | active, inactive or all |
adType | all | or political_and_issue_ads, housing_ads, employment_ads, financial_products_and_services_ads |
mediaType | all | image, video, meme, image_and_meme, none |
platforms | all | any of facebook, instagram, messenger, audience_network, threads, whatsapp |
languages | all | ad language codes, e.g. ["en","es"] |
exactPhrase | false | match the search term as an exact phrase |
sortBy | impressions | impressions (high → low, Meta's default) or newest |
includeAdDetails | true | EU/UK reach by country/age/gender, targeting, payer/beneficiary, advertiser Instagram (included in the price) |
includeLandingPage | false | product, price, compare-at price & discount, stock, rating and store platform from each ad's landing page (included in the price) — see "Products & prices behind the ads" |
saveCreatives | false | save each ad's image files (and video thumbnails) to this run's key-value store — $0.003 per file per started 5 MB, see "Creative archive" |
saveVideos | false | with saveCreatives: save the video file instead of its thumbnail (a 12 MB video = 3 × $0.003) |
maxCreativeMb | 25 | with saveCreatives: larger files are skipped and not charged (1-50) |
aiDigest | false | one AI "what changed" digest per search per run (monitor) — $0.05 per digest, only when something changed, see "AI daily digest" |
includeAiAnalysis | false | an AI read of each ad (hook, angle, offer, audience, tone…) — $0.005 per analysed ad, see "AI ad analysis" |
aiAnalysisLanguage | en | language of the analysis text fields; the labels (angle, offerType, ctaIntent, tone, sentiment) stay in English |
useResidentialForPaging | true | Page past the first 30 ads per query through Apify residential proxy (≈ $0.035 / 1,000 ads proxy traffic) |
activeFrom / activeTo | — | Meta's date filter: ads shown in this window (they may have started earlier) |
startedAfter | — | only ads launched on/after this date (new campaigns) — filtered ads are free |
startedWithinDays | 0 (off) | only ads launched in the last N days, counted from each run (for schedules) — filtered ads are free |
maxAdsPerQuery | 100 | cap per search / page / URL |
maxAdsTotal | — (none) | cap for the whole run across all searches, pages and links — the run never charges more ads than this |
minDaysActive | 0 | only ads running at least N days (winning ads) — filtered ads are free |
newWithinDays | 7 | window for the isNew flag |
onlyNewAds | false | monitor mode — see below |
watchlistId | — | name of a watchlist's memory (e.g. client-acme) — see below |
countOnly | false | one row per search × country with Meta's ad count instead of ad rows — $0.004 per row, see "Ad counts mode" |
countries | [] | with countOnly only: count every search in each of these countries (empty = country) |
proxyConfiguration | Apify Proxy | see "Depth & proxies" |
Example — products & prices behind long-running ads (dropshipping / e-commerce research)
{ "searchTerms": ["posture corrector"], "country": "US", "minDaysActive": 14, "maxAdsPerQuery": 150, "includeLandingPage": true }
Open the dataset view Products & prices for creative, product, price, discount, platform and days running side by side.
Example — winning ads in a niche
{ "searchTerms": ["protein powder"], "country": "US", "minDaysActive": 30, "maxAdsPerQuery": 200 }
Example — everything two competitors run, with EU audience data
{ "advertisers": ["gymshark", "https://www.instagram.com/allbirds/"], "country": "DE", "includeAdDetails": true }
Example — newest ads in a niche (daily monitor)
{ "searchTerms": ["ai note taker"], "country": "US", "sortBy": "newest", "onlyNewAds": true }
Example — weekly competitor watchlist (new + stopped ads, ad count trend)
{ "advertisers": ["huel"], "country": "GB", "watchlistId": "client-acme-huel", "maxAdsPerQuery": 400 }
Example — who else advertises with my brand name?
{ "searchTerms": ["huel"], "country": "GB", "excludeAdvertisers": ["Huel"], "maxAdsPerQuery": 200 }
Example — what launched in my niche this week (schedule weekly)
{ "searchTerms": ["protein powder"], "country": "US", "startedWithinDays": 7, "maxAdsPerQuery": 300 }
Products & prices behind the ads (includeLandingPage)
With includeLandingPage: true the Actor opens each ad's destination once per run (tracking parameters such as utm_… and fbclid removed, duplicates shared across ads) and reads the page's public product data (schema.org Product markup, Open Graph price tags, and for Shopify stores the public product JSON for the compare-at price). Each row gets landingProductName, landingBrand, landingPrice, landingCompareAtPrice, landingDiscountPct, landingCurrency, landingAvailability, landingRating, landingReviewCount, landingPageTitle, landingFinalUrl and landingPlatform (shopify, woocommerce, bigcommerce, magento, wix, squarespace, webflow, clickfunnels, gohighlevel, wordpress …).
- Included in the price — no extra charge per ad or per page; off by default, so existing runs and schedules are unchanged.
- Honest per row:
landingStatusisok,blocked(the site refuses automated visits — we never try to get around that),not-found,error,no-link(lead forms),skipped-app-link(App Store / Google Play — the platform is still named),skipped-on-platform(Messenger, WhatsApp, Facebook/Instagram pages),skipped-limitorskipped-time. A page that cannot be read never fails the run or drops the ad. - Polite & bounded: pages are read directly (no proxy), 2 at a time per site, up to 300 landing pages per run and 100 per site.
- A page without product markup (homepage, quiz, advertorial) still gives its title and platform; price and product stay
nullrather than guessed.
Creative archive (saveCreatives)
Meta signs every image and video link with an expiry time (mediaExpiresAt) — a swipe file built from those links is broken a few days later. With saveCreatives: true the Actor downloads each delivered ad's creative files straight from Meta's CDN into this run's key-value store and adds them to the row:
{ "advertisers": ["huel"], "country": "GB", "maxAdsPerQuery": 50, "saveCreatives": true }
"savedCreatives": [{ "type": "image", "key": "ad_925321173274919_1.jpg", "url": "https://api.apify.com/v2/key-value-stores/<storeId>/records/ad_925321173274919_1.jpg", "bytes": 184211, "contentType": "image/jpeg" }],"creativesSavedCount": 1, "creativeErrors": []
- What is saved: the ad's images (carousel cards included, up to 10 files per ad); for video ads the thumbnail — or with
saveVideos: truethe video itself (HD, or SD when HD is larger thanmaxCreativeMb). Keys aread_<adArchiveId>_<n>.<ext>. The output tab Saved ad creative files lists them; the dataset view Saved creatives shows them per ad. - Price: $0.003 per file per started 5 MB (event
media-saved): an image under 5 MB = $0.003 ($3 per 1,000 files), a 12 MB video = 3 events = $0.009. Charged only after the file is stored and its ad row is in your dataset. The same file used by several ads is downloaded and charged once per run. - Never charged: files that fail, are blocked, are larger than
maxCreativeMb(default 25 MB, max 50), or would go over your max charge per run — they are listed increativeErrorswith the reason. When your max charge is reached, saving stops; the ads you already have stay complete. - Example cost: 50 image ads with 1 image each = 50 × $0.0006 + 50 × $0.003 = $0.18.
- How long the files last: as long as the run's storage (Apify keeps run storages per your plan's data retention — copy them, or set up a named store / an integration, to keep them for good). The
urlopens the file directly; if your account restricts storage access, open it while signed in to Apify or with your API token. - Availability: this option becomes active once its
media-savedprice is enabled on this Actor. Until then a run withsaveCreativesdelivers the ads as usual, saves no files, charges nothing extra, and its status message says "saveCreatives is not enabled on this Actor yet".
AI ad analysis (includeAiAnalysis)
Reading 200 competitor ads to see which hooks, angles and offers they test takes an afternoon. With includeAiAnalysis: true every delivered ad gets an aiAnalysis object, written by Claude Haiku from the ad's copy, headline, CTA and carousel cards (plus the landing product and price when includeLandingPage is on):
{ "advertisers": ["huel"], "country": "GB", "maxAdsPerQuery": 100, "includeAiAnalysis": true, "includeLandingPage": true }
"aiAnalysis": { "status": "ok", "summary": "Huel sells complete-nutrition shakes to busy people with a first-order discount.","hook": "Lunch in 30 seconds", "angle": "feature-benefit", "offer": "15% off first order", "offerType": "discount","audience": "busy professionals", "painPoints": ["no time to cook"], "benefits": ["complete nutrition", "saves time"],"ctaIntent": "buy", "tone": "informative", "sentiment": "positive", "adLanguage": "en", "model": "claude-haiku-5-5" }
- Filterable labels:
angle(problem-solution, social-proof, discount-urgency, transformation, authority-expert, curiosity, comparison, lifestyle-aspiration, feature-benefit, fear-of-missing-out, storytelling, other),offerType,ctaIntent,tone,sentiment— count them per advertiser to see a competitor's creative strategy. The AI ad analysis table view shows them next to the copy. - Price: $0.005 per analysed ad (event
ai-analysis, $5 per 1,000), charged only after the ad row is in your dataset. 100 ads with analysis = 100 × $0.0006 + 100 × $0.005 = $0.56. - Never charged: ads without any words (image-only —
status: "no-copy"), failed or declined analyses, and ads over your max charge per run (status: "skipped", analysis stops, ad rows stay complete). aiAnalysisLanguage(es,de,id…) writes the summary, hook, offer and audience in that language.- The analysis reads text only — it does not look at the image or video itself — and states only what the ad says.
- Availability: paid Apify plans, once its
ai-analysisprice is enabled on this Actor (planned for early December 2026). Until then a run withincludeAiAnalysisdelivers the ads as usual, analyses nothing, charges nothing extra, and its status message says so.
AI daily digest (aiDigest)
Schedule this Actor every morning on the competitors you track and read one row per competitor instead of scrolling hundreds of ads. With aiDigest: true, after each search / advertiser is scanned Claude Haiku writes a digest row (rowType: "ai-digest"):
{ "rowType": "ai-digest", "query": "huel (page 282592881929497)", "country": "GB", "digestSince": "2026-10-28", "newAdsCount": 4, "stoppedAdsCount": 2,"aiDigest": { "headline": "Huel shifts budget to a 'meal, not a shake' breakfast push with a sub-£2 price anchor.","whatChanged": "…", "newAngles": ["…"], "newOffers": ["Under £2 per meal"], "hooks": ["\"Ready in 60 seconds\""], "formats": ["carousel"],"stoppedNote": "…", "notableNewAds": [{ "adArchiveId": "…", "why": "…", "libraryUrl": "…" }], "takeaways": ["…", "…", "…"] } }
- Only when there is news: a digest is written when the competitor launched or stopped ads since your last run, and on the first run (a baseline of their current strategy). Quiet days write nothing and cost nothing.
- Memory: uses the monitor memory. Give each client or competitor set its own
watchlistId; without one the digests keep the watchlistai-digest. Stopped ads are detected when the scan covers the advertiser's whole library (raisemaxAdsPerQueryabove their ad count). - Price: $0.05 per digest (event
ai-digest), charged only after the digest row is in your dataset. 10 competitors checked daily with ~5 busy days a week ≈ $2.50 a week. Ad rows are charged as usual. aiDigestLanguage(es,de,id…) writes the digest in that language. Combine withincludeAiAnalysisand the digest also reads each new ad's AI analysis.- The AI daily digest table view shows headline, what changed and takeaways per competitor.
- Availability: paid Apify plans, once its
ai-digestprice is enabled on this Actor (planned for early December 2026). Until then a run withaiDigestdelivers the ads as usual, writes no digest, charges nothing extra, and its status message says so.
Switching from another Ad Library scraper?
Keep your input. When a run has none of this Actor's own targets (searchTerms, advertisers, pageIds, adLibraryUrls, adIds), the field names other Ad Library scrapers use are understood: startUrls / urls (strings or { "url": … }; Ad Library links and facebook.com / instagram.com pages), searchQueries / keywords / query, pageIDs / pageUrls, resultsLimit / limitPerSource / maxAds (per search), count / maxItems (whole run → maxAdsTotal), scrapeAdDetails / isDetailsPerAd / fetchDetails, period / dateRange (last24h, last7d, last14d, last30d), onlyAdsNewerThan / onlyAdsOlderThan (dates or "7 days"), startDateMin / startDateMax, countryCode / searchCountries, adActiveStatus, searchType: "keyword_exact_phrase", newAdsOnly. The status message names every field it used. A setting you set yourself always wins, and an input that already uses this Actor's own fields is never changed. Output rows are this Actor's flat rows (see the table above).
Schedule it: competitor monitor
Turn on onlyNewAds and schedule the Actor daily or weekly (Console → Schedules). The Actor remembers every ad ID it has seen per query (in a named key-value store in your account) and delivers — and charges — only ads that are new since the last run. Connect the dataset to Slack, email, Google Sheets or a webhook and you have a free-running "competitor launched a new ad" alert.
With sortBy: "newest" (recommended for monitoring) the Actor stops each query as soon as it reaches the ads it already delivered last time — so a quiet day costs almost nothing, and older ads that the previous run simply did not reach are never sold to you as "new".
Tip: with the default impressions sort, set maxAdsPerQuery at least as high as the number of ads you want to watch (e.g. an advertiser's full active count), so every run covers the whole set.
Watchlists (watchlistId)
Give the monitor a name — one per client or project — and each watchlist keeps its own memory, so two clients watching the same brand never mix. With a watchlistId:
- every row carries
newSinceLastRunand Meta's ad count for the query now and last run (queryAdsTotal/queryAdsTotalPrev); the status message names the queries whose ad count changed; RUN_SUMMARY.queries[].stoppedSinceLastRunlists the ads that are no longer active since the previous run — switched off or removed from the library (free). It is only filled when the run saw the advertiser's complete active list (Meta had no further page and the ads cover Meta's total,activeStatus: "active"); otherwisestoppedChecksays why it was skipped — so a missing ad is never reported as stopped just because the run did not reach it;- with
onlyNewAdsoff you receive (and pay for) every ad each run, flagged new or not — a full weekly snapshot; withonlyNewAdson you pay only for new ads and still get the stopped list and the counts.
Without a watchlistId, onlyNewAds keeps using the memory it already had before 0.2.4.
Ad counts mode (countOnly)
When you need how many ads a search has, not the ads themselves, set countOnly: true. For each search × country the Actor opens only Meta's first page (no paging, no ad details, no ad rows) and returns one row with the total Meta reports for your filters.
Price: $0.004 per query × country (one query-count event per row). A search that fails, or for which Meta gives no total, is not charged. Your max charge per run is checked before each count is fetched, so a run never fetches or charges more counts than your limit allows.
{ "countOnly": true, "advertisers": ["huel", "myprotein"], "countries": ["GB", "DE", "FR", "US"], "watchlistId": "protein-brands-weekly" }
That is 2 advertisers × 4 countries = 8 rows = $0.032 (plus Apify's start fee per run).
| Field | Example | Notes |
|---|---|---|
rowType | count | Ad rows have no rowType |
query | huel → Huel | The search as you entered it (advertisers: name → page found) |
pageId, pageName | 282592881929497, Huel | For advertiser / page searches; null for keywords |
country, activeStatus, adType | GB, active, all | What was counted |
adsTotal | 1234 | Meta's reported total with your filters (country, activeStatus, adType, mediaType, platforms, languages, activeFrom/activeTo, exactPhrase). Searches with 0 ads get a row too |
adsTotalPrev, change | 1180, 54 | With watchlistId: last run's count for this search × country and the difference (null on the first run) |
firstPageNewestStart | 2026-10-17 | Latest start date among the ads on Meta's first page (up to 30 ads, in your sortBy order) — not the whole library |
firstPageFormats | {"image": 18, "video": 9, "dco": 3} | Ad formats on that first page ({} when there are no ads) |
checkedAt | 2026-10-19T08:00:00.000Z | When the count was read |
When a normal run is cheaper (honestly). Every ad row already carries Meta's total for its search in queryAdsTotal, and the free RUN_SUMMARY lists it per query. If you need the number for one country, a normal run with maxAdsPerQuery: 1 gets it for $0.0006 per search (one ad row), less than a sixth of the countOnly price. countOnly is worth $0.004 when you want:
- a row for searches with 0 ads, e.g. "this brand does not advertise on Meta in DE" as a line in your table (lead lists, market checks). A normal run gives such a search no row, only an entry in
RUN_SUMMARY; - many countries in one run:
countriescounts every search in every country, while a normal run searches one country; - a trend table: with
watchlistId, each row has last run's count and the change, ready for a weekly schedule into Sheets.
Filters that this Actor applies to ad rows (minDaysActive, startedAfter, startedWithinDays, excludeAdvertisers, onlyNewAds) cannot change Meta's count, so they are ignored in this mode and the log says so. Single-ad lookups (adIds) are not counted. The dataset view Ad counts shows the count columns. Counts are remembered per watchlist apart from the ad memory, so one watchlistId can be used for ad scans and counts.
Availability: this mode becomes active once its query-count price is enabled on this Actor. Until then a countOnly run ends SUCCEEDED with 0 rows, sends no request to Meta and charges nothing; its status message says "countOnly is not enabled on this Actor yet".
For AI agents & MCP
- Minimal input:
{"advertisers": ["<brand name>"]}or{"searchTerms": ["<keyword>"]}. Everything else has a sensible default. - Output: one row per ad, flat JSON, stable field names (table above).
winningScore,reachPerDayandisScaledanswer "which ads work";landingDomainanswers "where does the traffic go";RUN_SUMMARY.advertisersanswers "who advertises here". Ask the dataset API for only the fields you need (fields=pageName,title,winningScore,libraryUrl) to keep the answer small. - Price is predictable: $0.0006 per ad delivered, details (and landing pages, if you ask for them) included. Cap spend with
maxAdsTotal(whole run),maxAdsPerQueryor the run's max charge. Only need numbers?countOnlyreturns one row per search × country with Meta's ad count for $0.004 each (once enabled). Need the image/video files themselves?saveCreativesstores them in the run for $0.003 per file per started 5 MB (once enabled). - Empty is not an error: a keyword with no ads ends SUCCEEDED with 0 rows and a clear status message. Short hiccups are retried inside the run (fresh sessions after 20 s and 45 s); only a Meta outage that survives every retry ends FAILED so your pipeline can alert. Short run timeouts end SUCCEEDED with whatever was collected.
Depth & proxies (honest limits)
Meta serves the first page of every search (up to 30 ads) to anyone, but rate-limits deeper paging from datacenter IPs. The Actor handles this for you: page 1 and ad details go direct (free), deeper pages go through Apify residential proxy with automatic IP rotation when an exit IP gets limited. That traffic is small (≈ 0.4 MB per 100 ads) and appears on your Apify bill as residential proxy usage (≈ $0.035 per 1,000 ads). If your plan has no residential proxy, you still get the first page of every query and the status message says so. You can also plug in your own proxy in Proxy configuration.
Other limits: Meta shows spend/impressions only for political & issue ads, and reach/targeting data only for ads delivered in the EU or UK. Deleted ads and ads Meta hides from the library cannot be returned.
FAQ
Is this legal? The Actor reads only the public Ad Library that Meta publishes for transparency, without logging in. You are responsible for how you use the data (e.g. GDPR if you process personal data of EU persons).
Why is bodyText empty on a few ads? Some image-only or dynamic ads have no copy text. Their cards (cards) usually still carry title and link.
Why do image and video links stop working after a few days? Meta signs its media links with an expiry time. mediaExpiresAt shows it per row. Turn on saveCreatives to keep the files in your run's storage ($0.003 per file), download the creatives you want before then, or re-run the Actor for fresh links.
Is reachPerDay the ad's spend? No. Meta publishes reach only for ads shown in the EU/UK, and spend only for political & issue ads. reachPerDay is the published EU/UK reach divided by the days the ad has run — a comparison signal, not a budget.
Why is landingStatus "blocked" for some sites? Some big retailers refuse automated visits from cloud servers. The Actor does not try to get around that; the ad row is still delivered with its link, and landingPlatform / price stay null. Shopify and most direct-to-consumer stores answer normally.
Why do I get fewer ads than Meta's reported total? maxAdsPerQuery, filters (minDaysActive, startedAfter, startedWithinDays, excludeAdvertisers) and datacenter paging limits all reduce the count; RUN_SUMMARY.queries[].limitedBy tells you which one applied.
Related Actors
- Brand Ads Spy — Google + LinkedIn (
tactful_anvil/brand-ads-cross-network-spy) — the same brand across Google and LinkedIn ad libraries. - Ad Budget Signals (
tactful_anvil/ad-budget-signals) — does a list of domains run ads at all? One row per domain. - Google Ads Transparency Scraper (
tactful_anvil/google-ads-transparency-scraper). - Company Enrichment Scraper (
tactful_anvil/company-website-enrichment) — the company behind a landing domain: address, emails, phones, socials, full tech stack and ad pixels (one profile per domain).
Changelog
-
0.2.9 (live with its price, early December 2026) — new AI daily digest:
aiDigest(+aiDigestLanguage, off by default) adds onerowType: "ai-digest"row per search per run when ads were launched or stopped since the last run (and a baseline on the first run): headline, what changed, new angles / offers / hooks / formats, notable new ads, stopped ads, 3 takeaways — $0.05 per digest (eventai-digest); quiet days, failures and skips free; new "AI daily digest" view. Runs withoutaiDigestare unchanged. -
0.2.8 (live with its price, early December 2026) — new AI ad analysis:
includeAiAnalysis(+aiAnalysisLanguage, off by default) adds anaiAnalysisobject to each ad — summary, hook, angle, offer and offer type, audience, pain points, benefits, CTA intent, tone, sentiment — $0.005 per analysed ad (eventai-analysis); ads without words, failures and skipped ads free; new "AI ad analysis" table view. -
0.2.7 (2026-10-21) — new creative archive:
saveCreatives(+saveVideos,maxCreativeMb, all off by default) saves each delivered ad's image files, video thumbnails or videos into the run's key-value store, so they keep working after Meta's links expire; new row fieldssavedCreatives,creativesSavedCount,creativeErrors, dataset view "Saved creatives" and output tab "Saved ad creative files". Price: $0.003 per file per started 5 MB (new eventmedia-saved), only for files actually saved; becomes active once that price is enabled on this Actor. Runs withoutsaveCreativesare unchanged: same rows, same fields, same price. -
0.2.6 (2026-10-21) — new
includeLandingPage(off by default, included in the price): the product, brand, price, compare-at price & discount %, currency, stock status, rating and store platform behind each ad's link, one request per unique landing page per run; new dataset view "Products & prices". NewmaxAdsTotal: one cap on ads (= charged rows) for the whole run. Inputs written for other Ad Library scrapers (startUrls,urls,resultsLimit,count,period,scrapeAdDetails,onlyAdsNewerThan…) are now understood when the run has none of this Actor's own targets — before, such a run ended with 0 ads. Runs that already use this Actor's fields are unchanged: same rows, same fields, same price. -
0.2.5 (2026-10-19) — new Ad counts mode (
countOnly,countries): one row per search × country with Meta's ad count (0 included), the formats and newest start date on Meta's first page, and with awatchlistIdlast run's count and the change. $0.004 per row (eventquery-count); the mode becomes active once that price is enabled on this Actor, and until then acountOnlyrun ends with 0 rows and charges nothing. New dataset view "Ad counts"; the dataset schema no longer requiresadArchiveId, because count rows have none. Runs withoutcountOnlyare unchanged: same rows, same price. -
0.2.4 (2026-10-18) —
winningScorechanged (v2): it now uses reach per day (EU/UK) and "same message in several ads", and is computed after ad details are merged; on two real UK samples (Oct 2026, 99 ads) the new score follows reach per day (rank correlation 0.60, 0.79 and 0.72 per query; the old score: −0.26, −0.25 and 0.02). Scores of existing ads will differ from earlier runs and spread wider (in the 18 Oct sample the median fell from 72 to 61) — if you filter on a fixed score, re-check the threshold. New free fields:reachPerDay,reachMalePct,isScaled,copyVariantsInRun,creativeKey,creativeCopiesInRun,copyAdvertisersInRun,mediaExpiresAt,impressionsRange,advertiserPoliticalSpend,queryAdsTotal,queryAdsTotalPrev,newSinceLastRun. New inputs:watchlistId(watchlists with new / stopped ads and ad-count trend),startedWithinDays(relative launch-date filter),excludeAdvertisers(skipped ads are free). FreeADVERTISERSCSV and new dataset views (winners, swipe file, monitor, political). Rows of a query are now saved together when the query is complete (in chunks of 1,000 for very large queries — needed for the per-query score); the max-charge limit is respected as before. Price unchanged; no field was removed or renamed. If an account's monitor store belongs to another Actor (this Actor runs with limited permissions), the monitor memory moves to a second store instead of ending the run with 0 ads.- Fixed before release (independent review of 0.2.4; price unchanged): with
onlyNewAdsor awatchlistId, ads a run listed but did not deliver because of your max charge limit ormaxAdsPerQueryare no longer remembered as seen, so the next run delivers them; rows waiting for their query to finish now count against the max charge limit until they are charged, so a slow dataset write can no longer let a run take rows over the limit; a graceful abort now saves and charges those waiting rows first (a platform migration does not — the restarted run scans again); and when paging could not resume after an error (the re-opened first page had no next page) the scan is marked incomplete (limitedBy: "paging-interrupted"), so it can no longer list ads as stopped that it simply did not reach.
- Fixed before release (independent review of 0.2.4; price unchanged): with
-
0.2.3 (2026-10-18) — the example keyword "nike" that the input form pre-fills is no longer searched or charged when you also give advertisers, page IDs or Ad Library links (the run log says it was ignored). Typing "nike" on its own, or next to other keywords, still searches it. No price or field change.
-
0.2.1 (2026-09-28) — hardening from a 24-case cloud test matrix: runs with short timeouts now deliver (reserves scale with the run), 256 MB runs no longer run out of memory, one ad-details session per route (much less proxy traffic), one ad without details no longer switches details off for the rest of the run, monitor mode stops at last run's ads (newest sort), political
spendandreachEstimatefilled again (Meta switched them to text), deadtargetedCountriescolumn removed, patient retries before any failure, watchdog finishes the run before a platform timeout. -
0.2.0 (2026-09-25) — advertiser name/URL lookup, single ads by ID,
sortBynewest,rankInQuery+winningScore, ad details included (EU/UK reach, targeting, payer/beneficiary, advertiser Instagram), automatic residential paging with IP rotation, 3 queries in parallel, empty-page re-checks. -
0.1.0 (2026-09-25) — first release: keyword / page / URL search, SSR + GraphQL paging with automatic route fallback, dynamic-ad placeholder fix, winning-ad filter,
startedAfter, monitor mode, per-advertiser summary.