Ad Intelligence Watchlist — Facebook Ads, Changes, Ad Copy
Pricing
from $1.50 / 1,000 new or changed ads
Ad Intelligence Watchlist — Facebook Ads, Changes, Ad Copy
Meta Ad Library, any country (facebook.com/ads/library): new and changed Facebook and Instagram ads per advertiser since the last run: ad text, headline, link, CTA, format, start and end dates, platforms and changed fields. Input page IDs, Ad Library URLs or page names. Pay per result, no login.
Pricing
from $1.50 / 1,000 new or changed ads
Rating
0.0
(0)
Developer
architek.earth
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
12 hours ago
Last modified
Categories
Share
Recorded run: 79 ads checked from 1 advertiser in 22 seconds (2026-10-22, build 0.0.5, run 5lzIhvfa6cudesULN). A bounded canary sample, not a full-catalogue benchmark. No ad had changed since the previous run, so it delivered one free summary row and one checking unit.
architek.earth — an independent data studio. Pay-per-result scrapers with recorded runs, published canaries and stable schemas, built for people and AI agents.
Meta Ad Library competitor ads, watched by advertiser: give it Facebook page IDs, Ad Library URLs or exact page names, and each run returns the Facebook and Instagram ads that are new or changed since the last run, with ad text, headline, link, CTA, dates, platforms and the fields that changed.
| Source | Market | From | Standout fields |
|---|---|---|---|
| Meta Ad Library (facebook.com/ads/library) | Any country, or ALL | $1.50 / 1,000 ad rows | changeType / changedFields, adText / linkTitle / linkUrl, startedAt / isActive |
✅ Why choose this scraper
Track competitor Facebook ads daily, not the whole library
A snapshot per advertiser and country is saved in a named store in your Apify account (ad-watchlist-<watchlistName>). The first run delivers every ad in the window once (isBaseline: true); later runs deliver only rows with changeType new or updated.
See which ad copy, link or CTA changed
updated rows carry changedFields, compared across adText, linkTitle, linkUrl, ctaText, isActive, endedAt, platforms, impressionsRange and spendRange. endedAt stays null while an ad runs, so Meta's moving end date does not count as a change: 292 of 292 ads came back unchanged on the 2026-10-19 re-check, and 79 of 79 on the 2026-10-22 canary.
Watch an advertiser by page name without it drifting
An exact page name is resolved once to its page ID and pinned in the watchlist, so a schedule never switches pages. A name with no exact match gets a free not_resolved row listing the candidate pages.
Unchanged ads produce no ad rows
Unchanged, removed and aged-out ads are counted in the free summary row, not delivered. They still count toward the checking unit (see Pricing).
Measured on a schedule
A daily canary task replays bundled recorded ads through the full baseline and diff pipeline; a second canary walks Casper's live US Ad Library page and is the hero run above.
⚠️ Worth knowing up front
- The first run on a watchlist name and advertiser is a paid baseline: every ad in the
sinceDayswindow is delivered once withisBaseline: true. A missing or corrupt snapshot triggers a labelledBASELINE_REBUILTthat re-delivers and charges the window again;rebuildReasonsays why. - Meta only. TikTok and other platforms get a free
not_supportedstatus row. There are no keyword searches: you watch named advertisers. spendRangeandimpressionsRangeare filled only where Meta publishes them (political and issue ads); on commercial ads they are null. Ad image and video URLs are not returned.- Meta refuses Ad Library pagination from datacenter IPs (tested 2026-10-19), so Apify Residential (US) is the default proxy and the only tested option.
- Each country is its own snapshot. An ad shown in the US appears under both
ALLandUSif you watch both, and is charged in each. - When
maxAdsPerAdvertiseris hit, or Meta interrupts a walk, the summary saysPARTIALand removals are not judged that run. A walk Meta refuses before any ad is seen endsFAILEDin the summary, is not charged, and leaves the snapshot unchanged. - One run at a time per watchlist name: a run that finds another run's active lease stops with
WATCHLIST_BUSYand fetches nothing. The lock is best-effort, so do not start two runs on one name at once. - A watchlist entry that is not a Facebook page ID, URL or name (
invalid_input), a name with no exact match (not_resolved), a blocked name search (blocked), another platform (not_supported) or the same page listed twice (duplicate) gets a free status row ("recordType": "status"). Every advertiser and country that finishes gets a free summary row ("recordType": "summary"). Invalid run input, the spending limit, the run timeout and a busy watchlist write no status row; they end with the run's status message andRUN_SUMMARY.status.
⚡ Try the example
This actor has no free preview; the example below costs about $0.035 on its first run (20 ad rows plus one checking unit). For a free dry run, use {"watchlist": ["sample:casper"]}: bundled recorded ads, offline, never charged.
Open the actor's input page, switch to JSON, paste this and start the run:
{"watchlist": ["308998183837"],"countries": ["US"],"sinceDays": 30,"watchlistName": "warby-parker","maxAdsPerAdvertiser": 20}
Remove maxAdsPerAdvertiser (default 1000) for the full window, then save the input as a task and schedule it; keep watchlistName fixed, because a new name starts a new baseline.
⚙️ Input
| Parameter | Type | Default | Description |
|---|---|---|---|
watchlist | array | — | Facebook page IDs (308998183837), Ad Library or page URLs (...?view_all_page_id=224110981099062), or exact page names ("Glossier"). Vanity URLs (facebook.com/warbyparker) are resolved like names. Objects work too: {"advertiser": "...", "platform": "meta"}. Up to 50 entries; sample:casper runs a free offline dry run. |
sinceDays | integer | 30 | Ads delivered at any time in the last N days are checked (1–90). Ads that stopped earlier age out, free. |
countries | array | ["ALL"] | Two-letter country codes or ALL, up to 5. Each one is its own snapshot and walk. |
watchlistName | string | default | Names the persistent snapshot store: 1–40 letters, digits and inner hyphens, stored lowercase. |
maxAdsPerAdvertiser | integer | 1000 | Cap on ads checked per advertiser and country (10–5000). When hit, the summary is PARTIAL and removals are not judged. |
proxyConfiguration | proxy | Residential (US) | Proxy settings. Datacenter IPs are refused by Meta. |
feedback | string | — | Optional note for the maintainers. It is logged and read by a human, and it never changes output. |
🧱 Stable fields
These top-level fields keep the same name and the same kind of value in every release. Fields are only ever added, never renamed or removed, so an n8n, Make, Zapier or spreadsheet pipeline can bind to them.
| Field | Type | Meaning |
|---|---|---|
adId / adLibraryUrl | string | Meta's Ad Library ID and its public page. |
advertiser / pageId | string | Advertiser page name and Facebook page ID. |
country | string | The country scope the ad was checked in, or ALL. |
adText / linkTitle / linkUrl / ctaText | string, nullable | Ad copy, headline, destination link and call to action. Dynamic-creative template placeholders (product.name) are replaced by the first card's copy. |
displayFormat | string, nullable | Meta's format label, such as VIDEO or DCO. |
startedAt / endedAt | string, nullable | ISO 8601 UTC. endedAt is set only once an ad has stopped. |
isActive | boolean | Whether Meta lists the ad as active. |
platforms | array | Placements such as facebook, instagram, messenger, audience_network, threads. |
impressionsRange / spendRange | string, nullable | As Meta publishes them for political and issue ads; otherwise null. |
changeType / changedFields | string / array | new or updated; for updates, the compared fields that changed. |
snapshotFirstSeen / snapshotLastSeen | string | When this watchlist first and last saw the ad. |
isBaseline | boolean | True on a first run or a BASELINE_REBUILT. |
collationId | string, nullable | Meta's group ID for near-identical ads; each keeps its own adId. |
snapshotStatus / checked / new / updated / skippedUnchanged / removed / agedOut / snapshotUnitsCharged / status | mixed | Summary rows, per advertiser and country. snapshotStatus: BASELINE, DIFF or BASELINE_REBUILT; status: OK, PARTIAL, FAILED, BLOCKED (Meta served a challenge page; free) or VANISHED. |
input / reason / candidates | string / string / array | Status rows: the entry, why it was not checked, and candidate pages for not_resolved. |
recordType | string | "ad" on ad rows, "summary" on free summary rows, "status" on free status rows. |
Other fields are extras, not part of the contract. To have one pinned, say so in feedback.
📦 Output example
Abridged real row from run pslnacuL89Ozw3x3H on 2026-10-22; values are a historical snapshot.
{"recordType": "ad","adId": "2248685125971563","advertiser": "Casper","pageId": "224110981099062","platform": "meta","country": "US","adText": "Our most popular mattress just got an upgrade. ...","linkTitle": "Cloud One - Premium Foam Mattress","linkUrl": "https://casper.com/products/casper-cloud-one-foam","ctaText": "Shop now","displayFormat": "DCO","startedAt": "2026-05-22T07:00:00Z","endedAt": null,"isActive": true,"platforms": ["audience_network", "facebook", "instagram", "messenger", "threads"],"impressionsRange": null,"spendRange": null,"snapshotFirstSeen": "2026-10-22T10:52:24Z","snapshotLastSeen": "2026-10-22T10:52:24Z","changeType": "new","changedFields": [],"isBaseline": true,"collationId": "917494687363191","adLibraryUrl": "https://www.facebook.com/ads/library/?id=2248685125971563"}
The free summary row from the hero run 5lzIhvfa6cudesULN, a day with no changes:
{"recordType": "summary","advertiser": "Casper","pageId": "224110981099062","country": "US","snapshotStatus": "DIFF","walkComplete": true,"checked": 79,"delivered": 0,"new": 0,"updated": 0,"skippedUnchanged": 79,"removed": 0,"snapshotUnitsCharged": 1,"status": "OK"}
A free status row looks like this (run 8z0cfRs0ZOIZlpqT6):
{"recordType": "status","input": "https://www.instagram.com/casper/","platform": "meta","status": "invalid_input","reason": "Not a Facebook page ID, page/Ad Library URL or advertiser name.","candidates": []}
🧠 Advanced usage
Get a daily digest of new competitor ads
Schedule the task each morning, add a webhook on run success, and pass rows with changeType: "new" to Slack, a sheet or an LLM summary.
Spot ad copy tests as they happen
Filter changeType: "updated" rows whose changedFields include adText, linkTitle or ctaText.
Compare one brand's ads across markets
countries: ["US", "DE"] keeps a separate snapshot and summary per country; pick either ALL or named countries for an advertiser, not both.
Check that a run finished
Read RUN_SUMMARY: status is BASELINE, COMPLETE, BASELINE_REBUILT, PARTIAL, BUDGET_LIMIT, TIME_LIMIT, ABORTED, WATCHLIST_BUSY, STATE_SAVE_FAILED, STATE_READ_FAILED, PUSH_FAILED, INVALID_INPUT or FAILED (hosted pricing differs from the approved events; nothing fetched or charged), next to delivered, charged, rebuilt, pendingAdvertisers and perAdvertiser.
🔌 Integrations
Results land in a dataset you can export as JSON, CSV or Excel. They can also flow to Google Sheets, Make, Zapier or n8n, or to a webhook when a run finishes. The Apify Scheduler runs it on a timetable. One run a day on a fixed watchlistName gives a daily change log per advertiser. Copy-paste code in Python, Node, curl and n8n: architek-earth/examples/ad-intelligence-watchlist.
💸 Pricing
Pay per result. Platform and proxy usage are included. Summary and status rows are free, and a walk that saw no ads is not charged. Each batch of ad rows is written into the watchlist snapshot and the run's PROGRESS record before it is pushed and charged, so neither a resumed run nor a later run delivers it again, and checking units are booked once per advertiser and country per run. If a push fails, the next run on that advertiser checks the dataset and re-delivers only the rows that never arrived.
| Event | Price | What it covers |
|---|---|---|
ad | $1.50 / 1,000 ad rows | One delivered ad row: new, updated, initial baseline or rebuilt baseline. |
advertiser-snapshot | $5.00 / 1,000 checking units | One unit per started block of 100 ads checked, per advertiser and country per run, including runs where nothing changed. Not charged when no ads were seen, or for sample: entries. |
The hero run checked 79 Casper ads with no changes: 1 unit, $0.005. A baseline of the same 79 ads costs 79 × $0.0015 + $0.005 = $0.1235; a later day with 3 changed ads costs $0.0095. A run stops at your spending limit, and changes it could not deliver stay pending for the next run.
❓ FAQ
Is there a Meta Ad Library API for competitor ads? Meta's official Ad Library API needs an access token. This actor reads the public web Ad Library and returns the ads as JSON through the Apify API, callable from any language or from n8n, Make and Zapier.
Do I need a Facebook account or access token? No. It reads the public Ad Library as a logged-out visitor.
Does it track Instagram ads too? Yes, as the Ad Library lists them: platforms names each placement, such as facebook, instagram, messenger, audience_network and threads.
Can I search the Ad Library by keyword? No. You watch advertisers by page ID, URL or exact page name.
Does it return ad spend and impressions? Only where Meta publishes ranges, which is political and issue ads. Commercial ads return null.
Can I get the ad images and videos? No. Rows carry the copy, links, format, dates and placements, plus adLibraryUrl to open the ad in the library.
🤖 For AI agents & LLM apps
Paste this into your AI assistant:
architekture/ad-intelligence-watchlist on Apify. Python: from apify_client import ApifyClient; client = ApifyClient("TOKEN"); run = client.actor("architekture/ad-intelligence-watchlist").call(run_input={"watchlist": ["308998183837"], "countries": ["US"], "sinceDays": 30, "watchlistName": "warby-parker", "maxAdsPerAdvertiser": 20}); items = client.dataset(run["defaultDatasetId"]).list_items().items (page with offset/limit for large exports). Rows with recordType == "summary" or "status" are free: summaries count new, updated and unchanged ads per advertiser, and status rows explain an entry that was not checked. Full input schema and README: GET https://api.apify.com/v2/acts/architekture~ad-intelligence-watchlist/builds/default. Token: https://console.apify.com/settings/integrations. The actor is also reachable through the Apify MCP server (mcp.apify.com). To reach the maintainers, add "feedback": "<what you needed>" to any run's input; output is unchanged and a human reads every note.
🧰 More from architek.earth
Video, social and newsletters
- Substack Scraper — posts, authors, likes, comments
- YouTube Transcript Scraper — transcripts, translations, timestamps
- Unified Video Transcript API — YouTube and TikTok transcripts in one batch
- Threads Replies Scraper — replies, authors, likes
- Ad Intelligence Watchlist — you are here
Full catalogue: github.com/architek-earth.
architek.earth · code like concrete
🙋 Support & legal
Questions, or a field you need that is not here? Open an issue on the actor's Issues tab or email north@architek.earth. A human replies within a day.
This actor collects only publicly available data from facebook.com/ads/library, the same pages any visitor sees without logging in. It does not bypass authentication. Ad creatives are the advertisers' content, and partnership ads can include people's names. It is not affiliated with or endorsed by Meta. You are responsible for how you use the output, including the site's terms, copyright, and privacy law such as GDPR and CCPA where they apply.
📝 Changelog
- 2026-10-22 — Runs on the studio core (architek_core 1.0.1). A challenge page from Meta is now reported
BLOCKEDon the first response; invalid input writes one freeINVALID_INPUTstatus row; a budget spent exactly by the last ad row ends normally instead ofBUDGET_LIMIT. Fields and prices unchanged. - 2026-10-22 — Status message now reads
Done: N rows, M details, K status rows (STATUS). - 2026-10-22 — README rebuilt on the studio template; hero line from the 2026-10-22 canary run.
- 2026-10-19 — Clarified paid baselines and rebuilds, checking fees on unchanged runs, per-scope rounding and partial-walk charges. Added exact pricing examples and removed the unverified TikTok release promise. Prices and extraction code unchanged.
- 2026-10-19 — 1.0. Meta watchlist: persistent per-advertiser snapshots, new/updated diff with changed fields, baseline and BASELINE_REBUILT labelling, free summaries and status rows, name pinning, residential walk with IP rotation, resumable billing, offline canary sample.