Meta Ad Library Spy — Winning & Scaled Facebook Ads | $4/1K
Pricing
from $3.88 / 1,000 ads
Meta Ad Library Spy — Winning & Scaled Facebook Ads | $4/1K
Collect public Meta Ad Library ads by keyword or page ID. Get ad records with creative text, media URLs, delivery signals and platforms. Run with keywords or page IDs; no Meta login or Graph API token required. Pricing: $0.004 per ad on Free.
Pricing
from $3.88 / 1,000 ads
Rating
0.0
(0)
Developer
Vitalii Bondarev
Maintained by CommunityActor stats
4
Bookmarked
154
Total users
31
Monthly active users
4 days ago
Last modified
Categories
Features
Share
Meta Ad Library Scraper
Collect public Meta Ad Library ads by keyword or advertiser page ID, with copy, media URLs, delivery signals and platforms; no Meta account or token is required. Enable Apify Proxy with the RESIDENTIAL group: without it, Meta can return blocked or empty pages, and Free-plan runs deliver at most 50 ads.
- Find scaled creatives with
is_scaled,collation_count,days_activeandlongevity_score. - Monitor competitor ads by page ID or search term across countries, including active and inactive ads.
- Build creative swipe files with copy, CTAs, destination links and inline image or video URLs.
- Research political and issue ads with spend, impression, reach and EU transparency fields.
- Export flat records containing advertiser, creative, delivery and platform data.
What you get
A field is null when the source does not provide it; arrays can be null or [] as noted.
| Field family | Fields | Type | What it contains |
|---|---|---|---|
| Ad identity | ad_archive_id, ad_snapshot_url, page_id, page_name | string | IDs, snapshot URL and advertiser; each can be null. |
| Advertiser profile | page_profile_uri, page_profile_picture_url, page_like_count, page_categories, byline | string, integer, array | Profile, audience, categories and disclaimer; profile values and byline can be null; categories can be []. |
| Delivery signals | is_active, ad_delivery_start_date, ad_delivery_stop_date, total_active_time, days_active, longevity_score | boolean, ISO 8601 string, number, integer | Boolean status; dates are YYYY-MM-DD or null; total_active_time is passed through or null. Positive numeric duration gives days_active = total_active_time // 86400; otherwise it uses start to stop or run date, never below 0. longevity_score is round(min(days_active / 90, 1.0), 3). |
| Scale signals | is_scaled, collation_count, collation_id | boolean, integer, string | is_scaled is true when collation_count is 3 or more; the other fields can be null. |
| Ad copy | ad_creative_bodies, ad_creative_link_titles, ad_creative_link_captions, ad_creative_link_descriptions | array of strings | Body, headlines, captions and descriptions; arrays can be null. |
| Destination and CTA | link_url, cta_text, cta_type, display_format | string | Landing page, call-to-action and format; each can be null. |
| Inline media | snapshot_images, snapshot_videos, media_count, card_count | array of strings, integer | Image/video URLs and counts; URL arrays can be null; counts are set, including 0. |
| Platforms | publisher_platforms | array of strings | FACEBOOK, INSTAGRAM, MESSENGER, AUDIENCE_NETWORK, THREADS; [] when none are provided. |
| Ad metrics | currency, spend_lower, spend_upper, spend_midpoint, impressions_lower, impressions_upper, impressions_midpoint, reach_estimate, has_metrics | string, integer, boolean | Published spend, impressions and reach; metric integers and reach_estimate can be null; reach uses the lower bound. |
| Targeting and trust | targeted_or_reached_countries, contains_digital_created_media, contains_sensitive_content, categories, entity_type | array, boolean, string | Targeting, content and classification; arrays are [] when empty; scalar values can be null. |
| Transparency | state_media_run_label, regional_regulation_data | string, object | State-media and regional disclosures; either can be null. |
| Data quality | parse_confidence, warnings | number, array of strings | Parsing confidence and warnings; warnings is [] or can contain missing_ad_archive_id, missing_page_name, missing_creative, missing_start_date or no_media_or_link. |
| Run metadata | scraped_at | ISO 8601 string | One UTC scrape time for every row in a run; always set. |
Sample output
The sample is shortened to selected fields from one dataset record.
{"ad_archive_id": "388120997678322","ad_snapshot_url": "https://www.facebook.com/ads/library/?id=388120997678322","page_id": "1497383683837785", "page_name": "adidas","is_scaled": false, "collation_count": null, "collation_id": null,"days_active": 601, "longevity_score": 1, "is_active": false,"ad_delivery_start_date": "2024-09-26", "ad_delivery_stop_date": "2026-05-20","ad_creative_bodies": ["Conheça e compre todas as coleções e os principais lançamentos da adidas pelo app!"],"link_url": "http://play.google.com/store/apps/details?id=com.adidas.app","cta_text": "Shop now", "cta_type": "SHOP_NOW", "display_format": "DPA","media_count": 6, "card_count": 6,"publisher_platforms": ["FACEBOOK", "INSTAGRAM", "AUDIENCE_NETWORK", "MESSENGER"],"has_metrics": false, "parse_confidence": 1, "warnings": [],"scraped_at": "2026-11-04T05:29:04.147984+00:00"}
Pricing
This Actor uses pay-per-event pricing. Prices below are for the Free plan.
| Event | Price |
|---|---|
apify-actor-start | $0.00005 per event on the Free plan |
apify-default-dataset-item | $0.004 per event on the Free plan |
The apify-actor-start event is charged once per GB of run memory, minimum one event. No price is shown for either the ad-record or ad-record-political event.
On the Free plan, 1,000 ads cost $4.00 and 10,000 ads cost $40.00, plus the start event(s) of each run.
You pay only for the listed events. This Actor does not use Pay per event + usage, so compute, proxy traffic and storage are not billed separately.
The $5 Free-plan credit covers 24 full runs (1,200 ads) at the 50-ad cap; the exact Free-plan result calculation is 1,249 ads assuming one start event.
How to use
- Enter
searchTerms, numericpageIds, or both. - Choose two-letter country codes; each term or page ID is searched for each country.
- Select the ad category, active status, media type and keyword match type.
- Set
maxResultsper search and country, andpageSizefor streaming batches. - Enable Apify Proxy with the
RESIDENTIALgroup inproxyConfiguration. - Run the Actor, review the records, and repeat through Apify Schedules; daily is a starting point.
{"searchTerms": ["nike"],"countries": ["US"],"adType": "all","activeStatus": "all","mediaType": "all","matchType": "broad","maxResults": 100,"pageSize": 25,"proxyConfiguration": {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}}
Export the dataset as JSON or CSV, or read it through the Apify dataset API.
Input parameters
| Parameter | What it does | Default |
|---|---|---|
searchTerms | Keywords; each runs separately. If this and pageIds are empty, the run uses nike. | Empty |
pageIds | Numeric advertiser page IDs, searched for every country. | Empty |
countries | Two-letter codes, trimmed and upper-cased. Invalid codes are skipped with a run-log warning; US is used if none remain. | ["US"] |
adType | all or political_and_issue_ads. | all |
activeStatus | all, active or inactive. | all |
mediaType | all, image, video, meme or none (text-only). | all |
matchType | broad or exact phrase matching for search terms. | broad |
maxResults | Maximum per term or page ID per country; 1–10000. Other values use the nearest limit; non-numeric values use 100. | 100 |
pageSize | Ads per streaming batch; 1–100. Other values use the nearest limit; non-numeric values use 25. | 25 |
docId | Persisted-query ID; empty extracts the live ID on each page. | Empty |
proxyConfiguration | Apify Proxy; a URL is requested per search attempt when set. | {"useApifyProxy": false} |
disableUsageStats | Turns off the run summary described in Usage statistics when true. | false |
Limits and edge cases
| Condition | What you see | What to do |
|---|---|---|
| Free Apify plan | At most the smaller of maxResults and 50 ads are delivered in total. | Use a paid Apify plan for uncapped output. |
| Several terms, page IDs or countries | Countries, then terms, then page IDs are processed; duplicate ad IDs are removed across the run. | Use distinct searches for separate result sets. |
| Every search is blocked or empty | The run succeeds with an empty dataset; the status is a sentence and OUTPUT.emptyReason gives the reason. SOURCE_EMPTY means only seen ads or the Free cap produced no new rows. | Broaden the search, try another country, use activeStatus=all, check filters, or enable RESIDENTIAL. |
| A search uses a proxy on its first page | Up to 4 attempts are made. | Enable RESIDENTIAL and run again later. |
| A search has no proxy on its first page | Up to 2 attempts are made, reusing the same address. | Enable RESIDENTIAL and run again later. |
| A proxy attempt is made | A fresh proxy URL is requested on each attempt. | Enable RESIDENTIAL and run again later. |
RESIDENTIAL sees 2 clean pages with no ads | The search stops and is reported as NO_MATCH_FOR_INPUT. | Check the keyword, page ID, country and filters. |
| Every attempt for a search fails | The search is reported as BLOCKED. | Enable RESIDENTIAL and run again later. |
An empty page is returned without RESIDENTIAL | The search is reported as BLOCKED. | Enable RESIDENTIAL and run again later. |
| A first-page failure occurs | Only a first-page failure raises the block. | Review the status and rerun the search. |
| The cost guard is checked before an attempt | The search can end early. | Try fewer inputs or rerun later. |
| A later page fails or returns no ads | That search ends; ads already read are kept and the final partial batch is delivered. It is not retried. | Review the dataset and rerun the affected search if needed. |
A batch reaches pageSize rows | The batch is pushed to the dataset. | Use pageSize to control held rows. |
| A search finishes with a partial batch | The final partial batch is pushed. | Use a smaller pageSize for smaller final batches. |
Fewer than pageSize rows are held | Held rows are neither pushed nor charged. | Use a smaller pageSize. |
| A row is pushed | It is charged immediately after the push. | Review the dataset and charging records. |
| An abort, timeout or crash occurs | Pushed rows stay; held rows are not written. | Use a smaller pageSize. |
| An abort, timeout or crash occurs before completion | No OUTPUT record is written because it is written only at the end. | Review rows already pushed. |
| Cost protection stops a partial run | Delivered ads remain; MARGIN_GUARD is written to the default key-value store and the status says results are partial. | Review the partial results and rerun with fewer inputs if needed. |
| Cost protection stops a run with ads | OUTPUT.stoppedEarly = "MARGIN_GUARD" and the run finishes normally. | Review the partial results. |
| Cost protection stops a run with search failures | OUTPUT.searchFailures lists the failures. | Review the listed failures. |
| Cost protection stops a run with zero ads | OUTPUT.emptyReason = "BLOCKED" and stoppedEarly = "MARGIN_GUARD"; the run fails. | Enable RESIDENTIAL and rerun later. |
FAQ
How much does it cost to scrape Meta Ad Library ads?
On the Free plan, apify-default-dataset-item costs $0.004 per event and apify-actor-start costs $0.00005 per event. On the Free plan, 1,000 ads cost $4.00 plus the start event(s) of each run.
Why did my run return no ads?
A blocked or empty run can succeed with an empty dataset. Check emptyReason in OUTPUT, broaden the search or filters, try another country, or enable RESIDENTIAL.
What should I do if Meta blocks requests?
Enable an Apify RESIDENTIAL proxy group in proxyConfiguration. Retry behavior is described in Limits and edge cases.
Is it legal or allowed to use this public data?
The Actor reads public data shown by the Meta Ad Library without logging in. Source terms and applicable law depend on your use case; review them for your planned use. This Actor does not provide a legal guarantee.
Usage statistics
This Actor creates a small, content-free summary at the end of each run, used only to monitor reliability and improve this Actor. No input values, credentials or scraped data are sent.
A copy of the exact record for your run is saved as USAGE_STATS in your own Apify key-value store.
To opt out, set disableUsageStats to true in the input. Nothing is sent then.