# Changelog of Facebook Ads Library Scraper — EU Reach, Agency & Contacts (`foxlabs/meta-ad-library-scraper`) Actor

- **URL**: https://apify.com/foxlabs/meta-ad-library-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/foxlabs/meta-ad-library-scraper.md

## Changelog

### 0.1 — 2026-10-01

First version.

- Inputs: search terms (all words or exact phrase), advertisers (Facebook page ID, page link or name) and Ad Library links (a search with its filters, an advertiser's page of ads, or a single ad). Filters inside a link win over the form's filters.
- Filters as on the website: country (or all countries), active / inactive / all, ad category (political and issue, housing, employment, credit, financial products), media type, platforms, ad languages, "shown in period" (last 7/30/90 days or custom dates) and order (relevance, most impressions, newest as Meta orders it).
- One row per ad: Library ID and link, active status, start and end date, days running, advertiser page (name, link, categories, likes, picture), platforms, format, ad text, headline, link description, display URL, call to action, link, landing URL (Facebook redirects unwrapped), landing domain, UTM parameters, image and video URLs, carousel / catalog cards, number of versions, Meta's AI-generated-media label, ad category.
- Not returned: Meta's `targeted_or_reached_countries` list. It was empty in all 1,000 ads of a test run and in every earlier sample; the EU fields carry the countries.
- Placeholders such as `{{product.name}}` are never returned as text. Catalog ads (Meta's DPA format) and dynamic-creative (DCO) ads show them in Meta's snapshot; the text of the ad's cards is used instead. `isCatalogAd` is true for DPA ads only.
- Links with tracking macros (`?utm_campaign={{campaign.name}}`) keep their address; only the macro parameters are dropped, and `utmParams` holds real values only.
- Political and issue ads: spend, impressions and audience-size ranges as Meta shows them, plus the numbers read from them (`spendLower`/`spendUpper`, `impressionsLower`/`impressionsUpper`; a value Meta gives as "<100" has no lower bound), "paid for by", and the audience split by age, gender and region. Meta shows no audience split for an ad under 100 impressions; such a row has `detailsStatus: "no-audience-data"`.
- EU transparency (on by default, in the ad price): total EU reach, the age × gender × country reach buckets as Meta publishes them, the countries reached, targeted locations, ages and gender, payer and beneficiary, and `paidByThirdParty` / `thirdPartyPayer` when a different organisation paid (usually an agency, sometimes a related company). `detailsStatus: "not-in-eu"` means Meta did not mark the ad for EU transparency.
- Advertiser info (option, off by default, in the ad price): from the page's About tab, the legal owner Meta confirmed and its country, plus the Instagram account and page verification for ads outside the EU (EU ads carry the Instagram account from their details anyway). With advertiser contacts on, the advertiser rows also get the page admins' countries, the page's creation date and name changes. Off by default because one extra request per advertiser doubled a broad search: 100 ads took 70.8 s with it and 33.8 s without (platform runs on 2026-10-01).
- Advertiser contacts (option): one row per advertiser with its website (from the ads' landing pages, display URLs and catalog links), the e-mails and phone numbers the website publishes (home page, then the legal notice / Impressum or contact page) and a roll-up of its ads in the run.
  - The display URL counts twice when the website is chosen, because it shows the advertiser's own site when the link goes to a shop or a hosted page.
  - Social networks, app stores, hosted funnel, form and booking pages and click trackers are never taken as the website. When an advertiser's ads lead only to such a page, its own site is taken from the display URL or from a link on that page that carries the advertiser's name.
  - The legal notice or contact page may be on another domain only when that domain carries the same name (praxisklinik-mundart.de for mundart-praxisklinik.de).
  - Perspective funnels (sites built in the browser) are read through their layout data, which names the legal-notice page; on a funnel hosted on the platform's domain this gives the advertiser's own site.
  - E-mails: the advertiser's own domain first, then related domains, then mailbox-provider addresses (t-online.de, gmx.de…) that carry the advertiser's name. Addresses of chambers, supervisory, government and data-protection offices, outside data-protection officers and role mailboxes (data protection, press, compliance) are dropped. Other addresses are kept only when nothing better was found.
  - Placeholders and made-up addresses are dropped: an ending that is no real top-level domain, `xxxxx@…`, sample names, stock-photo credits. E-mails written with HTML entities are decoded.
  - Phones: a date is never a phone number; one number written in two formats is kept once.
  - A site is tried on its own domain and on www, directly, then through datacenter proxy and last through residential proxy in the searched country (many dental sites sit behind Cloudflare). After a TLS or connection error a route is tried once more with a plain browser header set. Each advertiser's site gets 60 seconds in all.
- Text inside image ads (option): OCR with Tesseract models built into the image; nothing is downloaded at run time.
- Only new ads (option): a named key-value store remembers the ads each search delivered. The memory is per search (same term, advertiser or link with the same filters). Known ads are skipped and not charged; the search reads on until it has its maximum of new ads.
- Each ad is delivered and charged once per run, whichever search finds it first.
- Speed: ads are enriched in batches of 30 while the next result page loads. Up to 8 detail and About calls run at once per search, spread over 4 datacenter proxy sessions. Two searches run in parallel.
- Pricing: `ad` per delivered ad; `advertiser-contacts` per advertiser row with at least one e-mail or phone; `image-text` per ad whose picture gave text. Status rows (nothing found, an unknown advertiser, an invalid link, a failed search) and advertiser rows without contacts are free; their message says no ad was charged.
- Network: no login and no account cookies. Page loads, ad details and About calls use Apify datacenter proxy; result pages use Apify residential proxy pinned to the searched country, with a new IP when Meta answers "rate limit exceeded". A page load that does not answer within 15 seconds is tried again on a new session. If Meta changes its query IDs, the Actor reads them again from the page's scripts.
- `SOURCE_REPORT` record per run: per search the status, ads, pages, Meta's result count, duplicates, details and About outcomes and `stopReason` (`max-ads-reached`, `no-more-results`, `no-new-ads-on-3-pages`, `page-limit`, `page-failed`, `max-charge-reached`); per run the requests and rate limits. It is written when the run starts, every 30 seconds and after every search, with `complete: false` until the run ends. A run stopped from outside keeps its last report, at most 30 seconds old; the dataset is the exact record of what was delivered.
- An error nobody caught does not end a run silently. The run then:
  - logs the error code;
  - leaves a free status row with the reason;
  - writes `SOURCE_REPORT` with `fatal` and `complete: false`;
  - fails with the reason as its status message. Ads delivered before it are kept.
- The same code also runs as a Standby HTTP API with the same events; Standby is not enabled on this Actor.
