Meta Ad Library Monitor: New Competitor Ads
Pricing
Pay per event
Meta Ad Library Monitor: New Competitor Ads
Monitor the public Meta Ad Library for the advertisers you choose and get only the Facebook and Instagram ads that are new, changed or ended since your last run, with copy, CTA, landing page, media links and an Ad Library link. Schedule it for competitor ad-launch alerts. No login, no API token.
Pricing
Pay per event
Rating
0.0
(0)
Developer
SingletonOps
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
an hour ago
Last modified
Categories
Features
Share
Monitor competitors' Facebook and Instagram ads in the public Meta Ad Library and get only the ads that are new, changed or ended since your last run, with copy, CTA, landing page, media links and an Ad Library link. Schedule it for competitor ad-launch alerts. No login, no API token. Built by SingletonOps (a brand of Vibex LLC).
Covers every Meta surface in the Ad Library (Facebook, Instagram, Messenger, Audience Network, Threads). A changed ad means its copy, title, CTA, link, media or platforms moved; an ended ad stopped running. Every run also writes a summary, and an optional webhook pushes the changes to you. The actor only reads what anyone can see at facebook.com/ads/library.
What it does
- For every advertiser it opens the public Ad Library search (by page or keyword) with your country, active-status, platform and media filters.
- It pages through all results (30 per request) up to
maxAdsPerAdvertiser. - It compares the ads with the state saved in a named key-value store from the previous run (the same stateful diffing used by our RSS and Sitemap monitors).
- It outputs only
new,changedandendedads, plus onesummaryrecord, and optionally POSTs them to your webhook.
Use cases for PPC teams
- Know the day a competitor launches. Schedule every 6 hours or daily with a fixed
stateStoreName; each run outputs onlynew,changedandendedads. - Paid-social creative reviews. Feed new ads (copy, CTA, media links) into a swipe file or a Google Sheet for the weekly creative meeting.
- Spot offer changes fast.
changedads show edits to copy, title, CTA, link or media, often a new promotion or price test. - See what they killed.
endedads tell you which tests a competitor stopped, which is as useful as what they launched. - Agency client monitoring. One task per client and country; send the webhook to Slack, Zapier or Make so account managers see changes without logging in.
- Find proven winners. Turn on
longRunningOnly(or setminDaysRunning, e.g. 90) to keep only ads that have been running for 6+ months: the creatives a competitor keeps paying for. Every row hasfirstSeenDate,lastSeenDateanddaysRunning, so you can also just sort bydaysRunning. - See when a promotion really ended.
lastSeenDateis the ad's end date once Meta stops delivering it, so you know when a sale ad actually stopped, whatever the landing page still says. - Keyword watch. Use
keyword:advertisers to watch everyone advertising on a topic, not just known competitors.
Setup: how to monitor competitor Meta ads
- Open the actor and add your competitors to Advertisers (page ID, Facebook page URL, Ad Library URL or
keyword:search). Use the page ID to monitor a brand. Akeyword:search matches any advertiser whose ads mention the word, not just that brand. In a test run,keyword:allbirds(all statuses, US) returned 50 ads but only 6 came from Allbirds' own page (778794852137593); the rest came from other advertisers mentioning "allbirds". Find a brand's page ID in its Ad Library URL (view_all_page_id=...), or paste its Facebook page URL. Keepkeyword:for topic watches. When a keyword target is used, the run log, the status message and thesummaryrow (hints,matchedPages) suggest the page IDs it matched. - Pick a country and keep
activeStatus: active. - Set a fixed State store name for this monitor (for example
competitors-us). - Click Start. The first run saves a baseline (every current ad is output once as
newwithisBaseline: true, unless you turn that off). - Save the input as a task and add a Schedule (for example every 6 hours or daily). Every later run outputs only new, changed and ended ads.
- Optional: add a
webhookUrl, or connect Slack, Google Sheets, Zapier or Make through Apify integrations.
Input
| Field | Default | Notes |
|---|---|---|
advertisers | – (required) | Page IDs (15087023444), page URLs (https://www.facebook.com/Gymshark), Ad Library URLs (...?view_all_page_id=... or ...?q=...), or keywords. keyword: forces a keyword search. Max 100. |
country | US | 2-letter ISO code or ALL. |
activeStatus | active | active, inactive or all. |
platforms | all | facebook, instagram, messenger, audience_network, threads. |
mediaType | all | all, image, video, meme, image_and_meme, none. |
exactPhrase | false | Keyword targets only: search the exact phrase (Meta's keyword_exact_phrase, the query is sent in double quotes like the Ad Library UI) instead of any words. Page IDs and page URLs ignore it. On a keyword it starts its own baseline. Note: for a pasted Ad Library URL only q= is read; its search_type is not, so set this option for exact-phrase URLs. |
maxAdsPerAdvertiser | 500 | Safety cap (1–5000). Counts every ad checked, including ads the long-running filter drops. |
longRunningOnly | false | Long-running ads only: keep ads with daysRunning ≥ 180 (about 6 months). |
minDaysRunning | 0 (off) | Keep ads with daysRunning ≥ this many days (0–3650). With longRunningOnly too, the larger value wins. |
stateStoreName | meta-ad-library-monitor-state | Use one store per monitor/schedule. |
emitAllOnFirstRun | true | First run outputs every current ad as new with isBaseline: true. Set false for a silent baseline. |
emitEnded | true | Output ads that stopped running. |
webhookUrl | – | Receives { summary, items (≤200), truncated, datasetId } when something changed or an advertiser failed. |
proxyConfiguration | Apify RESIDENTIAL | Required in practice (see Anti-bot notes). |
maxRetries, requestTimeoutSecs, pruneEndedAfterDays | 4, 45, 90 | Advanced. |
Example:
{"advertisers": ["15087023444", "https://www.facebook.com/Gymshark", "keyword:allbirds"],"country": "US","activeStatus": "active","maxAdsPerAdvertiser": 500,"stateStoreName": "competitors-us","webhookUrl": "https://hooks.example.com/meta-ads"}
Long-running ads only (6+ months)
{"advertisers": ["15087023444"],"country": "US","longRunningOnly": true,"stateStoreName": "nike-long-running"}
The filter runs after daysRunning is computed. Ads below the bar (or without a Meta start date) are not output, not charged and not tracked; one that later crosses the bar shows up as new that run. Turning the filter on starts its own baseline for the same stateStoreName, so your existing monitor's state isn't affected. Ads the filtered monitor already tracks are still followed, so their changes and their end are reported.
Output fields
| Field | Description |
|---|---|
changeType, changedFields, previous | new, changed or ended; for changed, which fields moved and their old values |
adArchiveId, adLibraryUrl | Meta's ad ID and a direct link to the ad in the Ad Library |
pageId, pageName, pageUrl | The advertiser's Facebook page |
isActive, startDate, endDate | Whether the ad is running and its day-level run dates |
firstSeenDate, lastSeenDate, daysRunning | Ad longevity. firstSeenDate = delivery start (YYYY-MM-DD). lastSeenDate = the run date (UTC) while the ad is active, or its end date once it stopped. daysRunning = days from first to last, counting both days (same day = 1). null when Meta gives no date; never guessed |
endDateStale, dataNote | true + a short note when Meta says the ad is active but its endDate is already past (stale Meta data, not a parsing issue); otherwise false / null |
platforms | facebook, instagram, messenger, audience_network, threads |
bodyText, title, caption, linkDescription | Ad copy |
ctaText, ctaType, linkUrl | Call to action and landing page |
displayFormat, imageUrls, videoUrls, videoPreviewUrls, cards, cardsCount | Creative format and media (carousel / catalog cards included) |
collationId, collationCount, categories | Meta's grouping of near-identical ads, and ad categories |
firstSeen, lastSeen, detectedAt, isBaseline | When this monitor first and last saw the ad (timestamps, not the ad's own run dates; see firstSeenDate / lastSeenDate); isBaseline marks first-run rows |
advertiserInput, country | Which input and country produced the row |
The summary row also has, per advertiser:
| Field | Description |
|---|---|
status, complete, error, errorCategory | OK / PARTIAL / FAILED; complete: true means the list was scanned to the end, so ended ads were detected |
totalReported, adsSeen | The result count Meta reported, and the ads actually returned |
reportedGap | Informational only: totalReported - adsSeen when a complete scan saw fewer ads than Meta reported (Meta's count includes ads it doesn't show to logged-out visitors and collated copies). It does not make the scan PARTIAL, so ended-ad detection still runs. null otherwise |
hint, matchedPages | Keyword targets only: a reminder to use a page ID for brand monitoring, and the top 5 pages (pageId, pageName, ads) the keyword matched. The run-level hints array collects them |
Output example
One dataset item per new / changed / ended ad:
{"type": "ad","changeType": "changed","changedFields": ["bodyText"],"previous": { "bodyText": "Old copy" },"adArchiveId": "1107386451952079","pageId": "15087023444","pageName": "Nike","pageUrl": "https://www.facebook.com/nike/","isActive": true,"startDate": "2026-09-01T07:00:00.000Z","endDate": "2026-10-12T07:00:00.000Z","firstSeenDate": "2026-09-01","lastSeenDate": "2026-10-12","daysRunning": 42,"endDateStale": false,"dataNote": null,"platforms": ["facebook", "instagram"],"bodyText": "Run through summer with gear that can take the heat.","title": "Nike Air Monarch IV","caption": "NIKE.COM","linkDescription": null,"ctaText": "Shop now","ctaType": "SHOP_NOW","linkUrl": "https://www.nike.com/","displayFormat": "DPA","imageUrls": ["https://scontent...fbcdn.net/..."],"videoUrls": [],"videoPreviewUrls": [],"cardsCount": 6,"cards": [{ "title": "Nike Air Monarch IV", "body": "...", "linkUrl": "...", "ctaText": "Shop Now", "imageUrl": "...", "videoUrl": null }],"collationCount": null,"collationId": null,"categories": [],"adLibraryUrl": "https://www.facebook.com/ads/library/?id=1107386451952079","firstSeen": "2026-10-12T14:59:16.000Z","lastSeen": "2026-10-12T15:04:31.000Z","isBaseline": false,"advertiserInput": "15087023444","country": "US","detectedAt": "2026-10-12T15:04:31.000Z"}
changeType:
- new: first time this ad is seen for this search. On the first run these have
isBaseline: true. - changed: body, title, CTA, landing link (tracking params ignored), media set (fbcdn signatures ignored) or platforms changed, or an ended ad came back.
changedFieldsandpreviousshow what changed. - ended: Meta now shows the ad as inactive, or (with
activeStatus: active) it disappeared from a fully scanned list. For ads that vanished, the text fields come from the last known state and media URLs are empty, andfirstSeenDate/daysRunningarenull(the state doesn't keep the start date).
The last dataset item is a summary record (also saved as OUTPUT in the run's key-value store) with per-advertiser status (OK / PARTIAL / FAILED), counts, whether the scan was complete, and HTTP/anti-bot stats. A QUALITY_REPORT record has field completeness and speed.
Creative velocity (optional)
Turn on Add creative velocity to the summary (includeCreativeVelocity: true) to see how fast each advertiser ships new creative. It is off by default, adds no requests and no charges, and rides on the free summary row (plus a CREATIVE_VELOCITY key-value record). Per advertiser:
newAds7d: ads whose Meta start date is in the last 7 dayslast7d.formatMix: static / video / carousel counts and percentageslast7d.uniqueLandingPages+topLandingPages: landing pages by domain + path, without query stringslast7d.copyVariants: distinct headline + primary-text variantsscore(0-100) andband: volume 50 pts (20+ new ads a week = full), format mix 15, landing pages 20 (5+ = full), copy variants 15 (10+ = full). Band: 20+ new ads = top, 10-19 active, 3-9 slow, 0-2 stalledfinding: one line, e.g. "Gymshark launched 28 new ads in the last 7 days, 42.9% static, across 8 landing pages."
Counts are a floor when Max ads per advertiser stops the scan early (sampleComplete: false).
How the diff stays reliable
- Partial runs never invent "ended" ads. Disappearance counts as ended only when the advertiser's list was scanned to the end without errors. Pagination failures mark the advertiser
PARTIALand keep everything collected so far. - No noise from rotating data. Signed media URLs, tracking parameters, the daily-moving
endDateof running ads, and the card order of catalog (DPA) and dynamic-creative (DCO) ads are excluded from the fingerprint. - "Reported but empty" is never a complete scan. If Meta reports a result count above 0 but returns no ads (in the page and in the GraphQL first page), the advertiser is marked
PARTIALwitherrorCategory: REPORTED_BUT_EMPTY,complete: false: nothing is charged and no tracked ad is marked ended. If a request had already failed (e.g.RATE_LIMITED), that category is kept. Entries that could not be parsed show asPARSE. - State per search. The state key includes the advertiser, country, status, media type and platforms, so changing a filter starts a fresh baseline instead of a flood of false changes.
- Crash-safe ordering. Changes are pushed before state is saved, so a crash re-emits rather than drops changes.
- Ended ads are kept for
pruneEndedAfterDays(so a re-launch shows up aschanged), then forgotten.
How much does it cost?
Pay-per-event, so a quiet run costs almost nothing:
| Event | Price | When it's charged |
|---|---|---|
Run start (apify-actor-start) | $0.01 per run | Once per run |
new-or-changed-ad | $0.005 per new or changed ad | Each new or changed ad saved to the dataset |
Ended ads, unchanged ads, summary row | Free | Never charged |
The Pricing tab on the Store page is always the authoritative price.
Examples:
- Daily monitor, 10 competitors, 15 new or changed ads on a typical day: $0.01 + 15 × $0.005 = $0.085 per run, about $2.55 per month.
- A quiet day with no changes: $0.01.
- First run: with
emitAllOnFirstRun: true(default) every current ad is output once asnewwithisBaseline: trueand is charged as a new ad (500 current ads = $2.51). SetemitAllOnFirstRun: falsefor a silent baseline that outputs and charges no ads.
Set Max cost per run in the run options to cap your spend. When the cap is reached, the actor stops after the current advertiser and keeps everything saved so far.
For reference, platform usage (compute + residential proxy) in our test runs was roughly $0.03–0.06 per 1,000 ads checked at 256 MB.
Anti-bot notes
- Logged-out traffic from datacenter IPs is redirected to the Facebook login page. Residential proxy works.
- Each new session gets a 403 interstitial that POSTs to
/__rd_verify_…; the actor solves it over plain HTTP (no browser needed). - The first GraphQL pagination request in a new session often returns
1675004 Rate limit exceeded. The actor backs off and retries on a new residential IP, re-reading session tokens and resuming from the saved cursor. - The pagination
doc_idis read from Meta's live JS bundle (cached for 12 h) with a built-in fallback.
Limits
startDate/endDateare day-level dates from the Ad Library. For running ads,endDateis "today" and moves daily (and is sometimes stale), solastSeenDateuses the run date for active ads instead. When Meta lists an ad as active but itsendDateis more than 2 days in the past, the row getsendDateStale: trueand adataNote;isActivestays as Meta reports it.- Meta's dates are Pacific-time days (they arrive as 07:00 or 08:00 UTC), so
firstSeenDatematches the date shown in the Ad Library.lastSeenDatefor active ads is the UTC run date, which can be one day ahead of the Pacific date for evening runs. - Meta groups near-identical ads ("collation"). The actor outputs the representative ad, so a keyword search can report fewer ads than the count Meta shows. Meta's count can also include ads it doesn't serve to logged-out visitors (for example, ads still in review), so a complete scan can return fewer ads than reported. That difference is shown as
reportedGapand is not treated as an error. - Keyword targets match any advertiser that mentions the word. Use page IDs to monitor specific brands.
- With
maxAdsPerAdvertiserhit, ended-by-disappearance is skipped for that advertiser (status stillOK). - Spend/reach figures are only published for political/issue ads and EU deliveries and are not output in this version.
Alerts: Slack, email, Zapier, Make or your own endpoint
- Save the input as a task with a fixed
stateStoreNameand add a schedule (every 6 hours or daily). - Built-in webhook: set
webhookUrl. After each run that found something (or where an advertiser failed), the actor POSTs{ actor, summary, items, truncated, datasetId }with up to 200 new / changed / ended ads. Point it at a Slack incoming-webhook relay, Zapier or Make catch hook, or your own endpoint. Quiet runs send nothing, so there's no alert noise. - Apify integrations: on the task's Integrations tab, connect Slack, email, Google Sheets, Zapier, Make or n8n to receive the run's dataset (which contains only changes plus one
summaryrow). - Watch the
summaryrow: an advertiser with statusFAILEDorPARTIALmeans that monitor needs attention (usually a temporary Meta rate limit; the next run picks up where it left off, and partial runs never inventendedads).
FAQ
Do I need a Facebook account or API token? No. The actor reads only the public Ad Library.
How often does it check? Only when it runs. Schedule it in Apify (every 6 hours or daily works well for competitor tracking) and keep the same stateStoreName.
Why did the first run output every ad? That is the baseline (isBaseline: true). Set emitAllOnFirstRun: false for a silent baseline.
How is daysRunning counted? lastSeenDate minus firstSeenDate plus 1, so both days count: an ad that started today has daysRunning: 1. For active ads it grows by one each day; for ended ads it's fixed. If Meta gives no start date (or an inactive ad has no end date), it's null, and the long-running filter leaves that ad out. Our Google Ads Transparency scraper uses the same three fields and the same definition, so you can combine both datasets.
Does it show ad spend or reach? No. Meta publishes those only for political/issue ads and EU deliveries, and this version does not output them.
Can I export to CSV or Google Sheets? Yes. Every Apify dataset downloads as JSON, CSV, Excel or XML, and integrations push it to Google Sheets, Slack, Make, Zapier or n8n.
Is it legal? It collects only data Meta publishes publicly in the Ad Library. You are responsible for how you use it and for complying with Meta's terms and applicable law.
Legal
This actor only reads ads Meta publishes in the public Ad Library, without logging in or bypassing any access control. It doesn't target personal data, but results can include advertiser and Page names. Make sure your use complies with Meta's Terms and the laws that apply to you (for example GDPR). If you're unsure, consult your lawyer. See Apify's post on the legality of web scraping.
Support
Found a bug or need a field? Open an issue on the Issues tab.
Built and maintained by SingletonOps, a brand of Vibex LLC. Also from SingletonOps: Google Ads Transparency Scraper & Ad Monitor, RSS Change Monitor and Sitemap Change Monitor.