# Meta Ad Library Scraper — Facebook & Instagram Ads + EU Reach (`hyperbach/meta-ads-library-scraper`) Actor

Every ad a page runs on Facebook, Instagram, Messenger, WhatsApp and Threads as one flat row: headline, body, CTA, landing URL, image and video URLs, run dates, platforms. Optional EU reach and audience per ad, political spend ranges. Honest counts, only-new-ads monitoring, proxies handled.

- **URL**: https://apify.com/hyperbach/meta-ads-library-scraper.md
- **Developed by:** [Hyperbach](https://apify.com/hyperbach) (community)
- **Categories:** Social media, Integrations
- **Stats:** 3 total users, 1 monthly users, 99.1% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 ad creatives

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Meta Ad Library Scraper — Facebook & Instagram Ads + EU Reach

**Every ad a page is running on Facebook, Instagram, Messenger, WhatsApp, Audience Network and Threads — as one flat row per creative, with the copy, the landing URL, the media, the run dates and, when you ask, the EU reach.** Headline, body, description, call to action, image and video URLs, first and last shown, platforms, and for ads about social issues, elections or politics the spend, impressions and reach ranges Meta publishes with the funding entity. **[Watch a competitor on a schedule](https://apify.com/hyperbach/meta-ads-library-scraper/examples/follow-a-competitors-meta-ads-weekly) and get only their new ads** — into Slack or your inbox through Apify's own integrations, with ads you already have never returned or charged. Same column names as our [Google Ads Transparency Scraper](https://apify.com/hyperbach/google-ads-transparency-scraper), so one sheet holds both platforms. **Proxies are handled for you.**

### Why this scraper, not the other Ad Library actors

- **Every ad, not one per creative.** The Ad Library lists one ad for each group that shares a creative and text ("N ads use this creative") and counts them all: Shopify shows ~5,700 results behind ~1,750 groups. Most actors in this category page the list and stop at what it shows — the two biggest, curious\_coder and igolaizola, open every group and deliver 5,597 (measured 2026-09-20, both, identical sets). This actor opens every group too (`expandVariants`, on by default), walks an advertiser in the order that does not repeat itself, and continues past the source's cursor limit in media and platform slices. Switch `expandVariants` off for the creative-level view: one row per group with `collation_count`.
- **Typed columns for the figures that matter, not a nested node to unpick.** Ten actors already flatten Meta's record; the three most-used hand back the raw GraphQL node. What none of them types: the EU reach as `eu_total_reach`, the country × age × gender breakdown as a list, the audience as `eu_audience_age_min` … `eu_audience_locations`, the payer and beneficiary; the political ranges as `spend_min`/`spend_max`, `impressions_min`/`impressions_max`, `reach_min`/`reach_max` with the currency and `paid_for_by`. Carousel and dynamic creatives keep every card in `cards` and put the first one's copy in the top-level columns, so a text column is never blank when the creative has words somewhere.
- **What the ad says and shows, in the same row.** `transcribeVideos` turns the speech into `transcript`, its language, the first three seconds (`hook_3s`) and `speech_seconds` — a music-only video is checked and not billed. `ocrImageAds` reads the words baked into the artwork with a vision model. `analyzeAds` returns a typed read: hook, offer, angle, audience, CTA intent, tone, sentiment. `enrichLandingPage` follows the link to the page it lands on and reads its product name, price and currency. In this category those are four separate actors (steadyfetch, dltik, apify's e-commerce event, automation-lab's resolver), each with its own bill.
- **Monitoring that tells you what ended.** `onlyNewAds` skips every creative earlier runs delivered — not returned, not charged. Every row carries `first_seen_at` and `last_seen_at` from the same memory, and `includeEndedAds` adds an `ended` row for each creative a complete scan no longer finds. The memory is named after the targets, or after your `stateStoreName`.
- **Honest counts, and the limits named.** The status of every run says what was delivered next to what the page claimed, and why a walk stopped: the source's cursor limit (about 1,900 creatives in one order, measured), a filter that excluded everything, a spending cap. Nothing is presented as complete that is not.
- **A run that stops can be continued.** A checkpoint is written after every page. Resurrect a timed-out run, or pass its id as `resumeFromRunId`, and it carries on from the cursor with no creative delivered or billed twice and `maxAds` counted across both.
- **Files that outlive Meta's links.** `image_url` and `video_url` expire in about four days. `downloadMedia` copies the files into a key-value store you own, keyed by creative, and links them from `media_files`; a creative met again is linked to its existing file, not fetched or billed twice.
- **No start fee, and the proxies are ours.** Twenty-three actors in this category charge a start fee; this one bills per row only. The Ad Library limits datacenter IPs on their first request and each residential exit after a few hundred; this actor carries its own rotating residential exits — a walker per target and a pool for the details and variant calls — and moves to a fresh one the moment the source pushes back, resuming the same cursor.
- **Fast where it counts.** 500 Shopify ads with details in 103 s, against 122 s for curious\_coder and 297 s for igolaizola on the same input the same afternoon (2026-09-20); three advertisers walked in parallel, each on its own exit.
- **The same columns as our Google Ads Transparency Scraper.** `advertiser_id`, `creative_id`, `headline`, `body_text`, `destination_url`, `cta`, `first_shown`, `last_shown`, `days_shown`, `image_url`, `spend_min`, `region`, `ad_url` mean the same thing in both. Schedule both against a competitor and one sheet holds their Google and Meta advertising side by side.

### Who it's for

- **Performance marketers & agencies** — read a competitor's live creatives — the copy, the offer, the landing page, the formats they are testing — and how long each has been running.
- **Creative strategists & copywriters** — a corpus of ad copy that is proven in market, with run dates showing which messages survived, instead of a swipe file nobody knows performed.
- **Founders sizing up a market** — who is paying to acquire customers in a niche, on which platforms, with what positioning — evidence of a market with money in it, before you spend any.
- **Researchers, journalists & compliance teams** — political and issue advertising with spend, impressions, reach and the funding entity, and the EU transparency figures Meta publishes per ad.

### Quick start

**Every active ad one page runs**

```json
{
  "advertiserIds": [
    "20409006880"
  ],
  "region": "ALL",
  "maxAds": 100
}
```

**With EU reach and audience per ad**

```json
{
  "advertiserIds": [
    "20409006880"
  ],
  "includeEuReach": true,
  "maxAds": 50
}
```

**Political ads about a topic, with spend**

```json
{
  "searchTerms": [
    "election"
  ],
  "adType": "political_and_issue",
  "region": "US",
  "maxAds": 50
}
```

**Monitoring — only what appeared since the last run**

```json
{
  "advertiserIds": [
    "20409006880"
  ],
  "onlyNewAds": true
}
```

### Output

One record per ad creative:

| field | meaning |
|---|---|
| `row_type` | `ad` for a creative; `advertiser` for a page resolved from `advertiserNames` / `searchTerms` in `resultType: advertisers`; `count` for a claimed-total row in `resultType: counts`; `advertiser_profile` and `landing_page` for the rows `summaryRows` adds; `ended` for a creative a complete scan no longer returns (`includeEndedAds`). |
| `advertiser_name` | The page's name as the Ad Library shows it. |
| `advertiser_id` | The numeric Facebook page ID behind the ads. Join on this across runs and platforms; it is also what `advertiserIds` takes. |
| `creative_id` | The ad's Library ID — the `id` in an Ad Library URL. Unique per creative; the key for `onlyNewAds`. |
| `collation_id` | Meta groups near-identical creatives (the same ad run in several sizes or placements) under one collation. The group's id. |
| `collation_count` | How many ads share this creative and text — Meta's group ("N ads use this creative"). With `expandVariants` on, every one of them is a row and they share the `collation_id`; off, this row stands for the group. |
| `ad_format` | `image`, `video`, `dco` (a dynamic creative with per-card copy and media), `carousel`, `multi_images` and the other display formats Meta uses, lowercased. |
| `is_active` | Whether the ad was running when the run read it. |
| `headline` | The ad's title. For carousel and dynamic creatives, the first card's title when the top level has none. |
| `body_text` | The primary text. For carousel and dynamic creatives, the first card's body when the top level has none. |
| `link_description` | The description line under the link, when the creative carries one. |
| `headlines` | Every card's title, for carousel and dynamic creatives. Empty on single-card ads. |
| `descriptions` | Every card's body text, for carousel and dynamic creatives. |
| `display_url` | The caption shown under the creative — usually the advertiser's domain. |
| `destination_url` | Where the ad sends a click. |
| `cta` | The call-to-action button's label, e.g. `Sign up`, `Shop now`. |
| `cta_type` | Meta's own CTA code, e.g. `SIGN_UP`, `SHOP_NOW`. |
| `cards` | For carousel and dynamic creatives, one entry per card: `headline`, `body_text`, `link_description`, `display_url`, `destination_url`, `cta`, `cta_type`, `image_url`, `video_url`, `video_preview_url`. |
| `first_shown` | The day the ad started running (UTC). |
| `last_shown` | The day the ad was last seen running — today for an active ad. |
| `days_shown` | Days between `first_shown` and `last_shown`, inclusive. Long-running ads are the ones that earn their budget. |
| `image_url` | The creative's image — the ad's own, or the first card's. A direct CDN link that fetches without a login and expires about four days after the run. |
| `image_urls` | Every distinct image URL the creative carries, in order. |
| `image_small_url` | Meta's ~600 px rendition of the first image — the one `ocrImageAds` reads; `image_url` is the original. |
| `video_url` | The creative's video, HD rendition where there is one. Same expiry as the images. |
| `video_urls` | Every distinct video of the creative, in order (multi-video and carousel creatives carry several). |
| `video_duration_s` | Length of `video_url` in whole seconds, read from the URL's own encode tag (`efg.duration_s`, present on 80 of 80 video ads measured 2026-09-20) — no extra request. Null for image ads. |
| `video_preview_url` | The poster frame of the video. |
| `media_files` | With `downloadMedia`: one entry per file copied into your key-value store — `kind` (image/video), `key`, `url` (the store URL, which does not expire), `bytes`, `content_type`, `source_url`, and `reused` (true when an earlier run already stored it — linked, not billed again). Empty otherwise. |
| `image_file_url` | With `downloadMedia`: the store URL of the first image. Null otherwise. |
| `video_file_url` | With `downloadMedia: all`: the store URL of the first video. Null otherwise. |
| `transcript` | With `transcribeVideos`: what is said in the video, as one text. Null when the video has no speech (then `speech_seconds` is 0) or when it was not transcribed. |
| `transcript_language` | With `transcribeVideos`: the spoken language as an ISO 639-1 code (`en`, `fr`, `ko`), as detected. |
| `hook_3s` | With `transcribeVideos`: the words spoken in the first three seconds of speech — the hook — counted from the first word, not from the start of the video. |
| `speech_seconds` | With `transcribeVideos`: seconds of speech in the video. 0 means the video was checked and carries no speech (music or silence); null means it was not transcribed. |
| `ad_ocr_text` | With `ocrImageAds`: the words a vision model read out of the creative's image (or a video's poster frame), in reading order. Null when the image carries no readable text or was not read. |
| `sentiment_score` | On every row, free: the mean AFINN-165 valence of the English words in the copy, scaled to -1 (negative) .. 1 (positive). Null when the copy has no scored word — a non-English ad is unscored, not neutral. |
| `analysis_hook` | With `analyzeAds`: the line meant to stop the scroll, quoted or closely paraphrased from the ad. |
| `analysis_offer` | With `analyzeAds`: the concrete thing on offer — product, discount, trial, free item — or null when the ad makes no specific offer. |
| `analysis_angle` | With `analyzeAds`: the persuasion angle — `problem_solution`, `social_proof`, `scarcity`, `discount`, `feature`, `lifestyle`, `curiosity`, `education`, `comparison`, `brand` or `other`. |
| `analysis_audience` | With `analyzeAds`: who the ad is written for, in the model's words (e.g. `small retailers opening a second location`). |
| `analysis_cta_intent` | With `analyzeAds`: what the ad wants done — `purchase`, `signup`, `download`, `learn_more`, `contact`, `visit`, `watch`, `apply`, `book` or `other`. |
| `analysis_tone` | With `analyzeAds`: `playful`, `urgent`, `professional`, `friendly`, `luxurious`, `informative`, `emotional`, `bold` or `other`. |
| `analysis_sentiment` | With `analyzeAds`: the model's read of the copy's emotional valence, -1 .. 1. |
| `landing_final_url` | With `enrichLandingPage`: the page the ad's link lands on after redirects, with tracking parameters (utm\_\*, fbclid, gclid …) stripped. Null when nothing answered. |
| `landing_domain` | With `enrichLandingPage`: the final page's domain, without `www.`. |
| `landing_status` | With `enrichLandingPage`: the HTTP status the final page answered (200, 404, 403 …); null when the site did not answer at all. |
| `landing_title` | With `enrichLandingPage`: the page's title (Open Graph title first, then `<title>`). |
| `landing_is_product` | With `enrichLandingPage`: true when the page declares itself a product (JSON-LD `Product`, `og:type` product, or a price in its markup); false for other pages; null when the page did not answer. |
| `landing_product_name` | With `enrichLandingPage`: the product's name from JSON-LD, or the page title on a product page. |
| `landing_price` | With `enrichLandingPage`: the price the page publishes for machines (JSON-LD offers, `product:price:amount`), as shown to the run's location — a storefront that localises by visitor answers in that country's currency. Null when the page publishes none. |
| `landing_currency` | With `enrichLandingPage`: the ISO currency of `landing_price`. |
| `platforms` | Where the ad ran: `facebook`, `instagram`, `messenger`, `whatsapp`, `audience_network`, `threads`. |
| `launched_last_7_days` | Whether the ad started running within the last 7 days of the run. |
| `launched_last_30_days` | Whether the ad started running within the last 30 days of the run. |
| `long_running` | Whether the ad has been shown for at least `longRunningDays` (default 30) — the creatives that survive are the ones that convert. |
| `platform_count` | How many Meta platforms the ad ran on. |
| `categories` | Meta's categories for the ad — `political` for ads about social issues, elections or politics; `unknown` for ordinary commercial ads. |
| `regions` | Countries the ad targeted or reached, when Meta lists them (political ads). |
| `region` | The country the run asked for — a code, `ALL`, or a comma-separated list when `regions` held several. |
| `spend_min` | Lower bound of the spend range Meta publishes. Political and issue ads only; null on commercial ads. |
| `spend_max` | Upper bound of the spend range; null when the range is open-ended (`>$1M`). |
| `spend_currency` | ISO currency of the spend range. |
| `impressions_min` | Lower bound of the impressions range. Political and issue ads only. |
| `impressions_max` | Upper bound of the impressions range; null when open-ended (`>1M`). |
| `reach_min` | Lower bound of the estimated reach range. Political and issue ads only. |
| `reach_max` | Upper bound of the estimated reach range; null when open-ended. |
| `paid_for_by` | The disclaimer's funding entity on a political or issue ad. |
| `funding_entity_website` | The funding entity's website, from Meta's verification record. |
| `funding_entity_address` | The funding entity's address, from Meta's verification record. |
| `eu_total_reach` | Accounts reached across the EU, as published under the DSA. Filled by `includeEuReach` on ads that reached the EU; null on the rest. |
| `eu_reach_breakdown` | Reach per EU country, age range and gender: entries of `country`, `age_range`, `male`, `female`, `unknown`. Filled by `includeEuReach`. |
| `eu_audience_age_min` | The youngest age the advertiser targeted in the EU. |
| `eu_audience_age_max` | The oldest age the advertiser targeted in the EU. |
| `eu_audience_gender` | The gender the advertiser targeted in the EU: `All`, `Men` or `Women`. |
| `eu_audience_locations` | The locations the advertiser included or excluded in the EU: entries of `name`, `excluded`, `type`. |
| `payer` | Who paid for the ad, as disclosed under the DSA. Filled by `includeEuReach`. |
| `beneficiary` | Who benefits from the ad, as disclosed under the DSA. |
| `political_reach_by_region` | For political ads with `includeEuReach`: the share of reach per region, entries of `region` and `reach_share` (0–1), regions with reach only. |
| `political_reach_by_age_gender` | For political ads with `includeEuReach`: the share of reach per age range and gender. |
| `advertiser_category` | The page's category, e.g. `Software`. Filled by `includeAdvertiserInfo` or `includeEuReach`. |
| `advertiser_about` | The page's About text. Filled by `includeAdvertiserInfo` or `includeEuReach`. |
| `advertiser_categories` | The page's categories as the Ad Library lists them, e.g. `["Software"]`. |
| `advertiser_ads_seen` | On an `advertiser` row: how many ads of the name search belonged to this page. |
| `advertiser_match` | On an `advertiser` row: how the page name matched the name searched — `exact`, `starts`, `contains` or `none`. |
| `advertiser_claimed_ads` | On an ad row from a page target: the total the Ad Library page claims for that advertiser — ads, not creative groups. With `expandVariants` on, the delivered count comes close; the status says what the source's cursor limit cut (see the FAQ). |
| `claimed_count` | On a `count` row: the total the Ad Library shows for the target, from one page load. |
| `summary_ad_count` | On a summary row: how many delivered ads it covers. |
| `summary_active_count` | On an `advertiser_profile` row: how many of them are active. |
| `summary_longest_days` | On an `advertiser_profile` row: the longest `days_shown` among them. |
| `summary_formats` | On an `advertiser_profile` row: `{format, count}` per ad format, most common first. |
| `summary_landing_domains` | Distinct landing-page domains behind the ads. |
| `summary_distinct_copies` | On an `advertiser_profile` row: how many distinct primary texts the ads use. |
| `summary_ad_ids` | On a summary row: the Library IDs of the ads it covers. |
| `advertiser_likes` | The page's like count. |
| `advertiser_ig_username` | The page's linked Instagram handle. Filled by `includeAdvertiserInfo` or `includeEuReach`. |
| `advertiser_ig_followers` | Followers of the linked Instagram account. |
| `advertiser_verified` | Whether the page carries Meta's verification badge. |
| `advertiser_spend_total` | For political pages: lifetime spend across the page's disclaimers, as Meta reports it. Filled by `includeAdvertiserInfo` or `includeEuReach`. |
| `advertiser_spend_week` | For political pages: spend in the last seven days. |
| `advertiser_url` | The page's Facebook URL. |
| `advertiser_picture_url` | The page's profile picture. |
| `ad_url` | The ad in the Ad Library, for a human to open. |
| `matched_query` | The keyword or Library ID that produced this row; null on rows from a page ID. |
| `matched_fields` | On a keyword row: which of the ad's texts contain the whole search term — `headline`, `body_text`, `link_description`, `destination_url`, `display_url`, `advertiser_name`, `cards`. Empty means the Library matched it loosely; `keywordRelevance: visible` drops those. |
| `first_seen_at` | With a monitoring memory: the day a run of these targets first saw the creative. |
| `last_seen_at` | With a monitoring memory: the day a run last saw it — today on a live row. |
| `ended_at` | On an `ended` row: the day a complete scan first failed to return the creative. |
| `run_tag` | The `runTag` you gave the run, on every row of every type — so rows from several runs sort in one sheet. Null when none was given. |
| `raw` | Meta's own record, untouched — only with `includeRawData`. |

Example record:

```json
{
  "row_type": null,
  "advertiser_name": "Shopify",
  "advertiser_id": "20409006880",
  "creative_id": "1657481092030542",
  "collation_id": "1308187987545965",
  "collation_count": 1,
  "ad_format": "dco",
  "is_active": true,
  "headline": "{{product.name}}",
  "body_text": "{{product.brand}}",
  "link_description": "{{product.description}}",
  "headlines": [
    "Vendez gratuitement sur Shopify",
    "Jetzt kostenlos auf Shopify verkaufen"
  ],
  "descriptions": [
    "Obtenez le bouton que tout le monde adore. Commencez à vendre et à gagner de l’argent avec Shopify 💰",
    "Der Button, den die Leute kennen und lieben. Jetzt verkaufen und mit Shopify verdienen 💰"
  ],
  "display_url": "shopify.com",
  "destination_url": "https://www.shopify.com/se/gratis-trial",
  "cta": "Sign up",
  "cta_type": "SIGN_UP",
  "cards": [
    {
      "headline": "Vendez gratuitement sur Shopify",
      "body_text": "Obtenez le bouton que tout le monde adore. Commencez à vendre et à gagner de l’argent avec Shopify 💰",
      "link_description": null,
      "display_url": "shopify.com",
      "destination_url": "https://www.shopify.com/fr/essai-gratuit",
      "cta": "Sign Up",
      "cta_type": "SIGN_UP",
      "image_url": null,
      "video_url": "https://video-mxp2-1.xx.fbcdn.net/o1/v/t2/f2/m366/AQOMRBFZd7QAA0ruNoYgN6JXjAiwMNc_Yicgl9hZIh8lOlORAPEtR50aqEB4yfQkm6GjRg_8HrdX7-RYLqyVQCzvuyhfySfpOGVKhVn39EzgmQ.mp4?_nc_cat=101&_nc_sid=b66105&_nc_ht=video-mxp2-1.xx.fbcdn.net&_nc_ohc=dEUkWdMHDwYQ7kNvwHiJ8XN&efg=eyJ2ZW5jb2RlX3RhZyI6Inhwdl9wcm9ncmVzc2l2ZS5WSV9VU0VDQVNFX1BST0RVQ1RfVFlQRS4uQzMuNzIwLmRhc2hfaDI2NC1iYXNpYy1nZW4yXzcyMHAiLCJ4cHZfYXNzZXRfaWQiOjgxNTc1MTk4NzU2ODUxNSwiYXNzZXRfYWdlX2RheXMiOjM1OSwidmlfdXNlY2FzZV9pZCI6MTAxMzksImR1cmF0aW9uX3MiOjEyLCJ1cmxnZW5fc291cmNlIjoid3d3In0%3D&ccb=17-1&vs=257c57522b2c310e&_nc_vs=HBksFQIYRWZiX2VwaGVtZXJhbC9CODQzMDVENjk4MkMzOThBMjBFRTg0MzRBMkUyRDg5N19tdF8xX3ZpZGVvX2Rhc2hpbml0Lm1wNBUAAsgBEgAVAhhAZmJfcGVybWFuZW50LzExNEI1QjA2OTQzNEMyQzVCMjVGODRERUJCQjJFQjhEX2F1ZGlvX2Rhc2hpbml0Lm1wNBUCAsgBEgAoABgAGwKIB3VzZV9vaWwBMRJwcm9ncmVzc2l2ZV9yZWNpcGUBMRUAACaG_oz3gfvyAhUCKAJDMywXQCgAAAAAAAAYGWRhc2hfaDI2NC1iYXNpYy1nZW4yXzcyMHARAHUAZbaeAQA&_nc_gid=CXhKjE0I-jAPsnY2HWAufw&_nc_ss=7e289&_nc_zt=28&oh=00_AQKP-fMbUXalHsS9wXs3LMR-_48s_pNZB8-Q4gsPXfn6Nw&oe=6AB52807",
      "video_preview_url": "https://scontent-mxp1-1.xx.fbcdn.net/v/t39.35426-6/710447423_2716333165389931_221950439945272582_n.jpg?_nc_cat=106&ccb=1-7&_nc_sid=c53f8f&_nc_ohc=Iq_OjuwglZMQ7kNvwFbTh7K&_nc_oc=Adpm8fPUfnukFoj49quE74I_yce8YnDZH9bV7imwtVDPiUtTJFYVW9EdKEwXNHklqYY&_nc_zt=14&_nc_ht=scontent-mxp1-1.xx&_nc_gid=CXhKjE0I-jAPsnY2HWAufw&_nc_ss=7e289&oh=00_AQI43eq2bcH5RMsal09q_k9dBmEqZ4ebUy7tpCOHT7Yc9Q&oe=6AB51813"
    },
    {
      "headline": "Jetzt kostenlos auf Shopify verkaufen",
      "body_text": "Der Button, den die Leute kennen und lieben. Jetzt verkaufen und mit Shopify verdienen 💰",
      "link_description": null,
      "display_url": "shopify.com",
      "destination_url": "https://www.shopify.com/de/kostenloser-test",
      "cta": "Sign Up",
      "cta_type": "SIGN_UP",
      "image_url": null,
      "video_url": "https://video-mxp1-1.xx.fbcdn.net/o1/v/t2/f2/m366/AQMzymmwQZ2QnkOpNdfkNUDVtUuEF_DCKrHfcjYtpPp0SVWyHsjV3IHAZNshaq4BvtK4tZKod6N2t8ZH1me2ehX7_coU5YYzbFp7Fqetyh3Fng.mp4?_nc_cat=107&_nc_sid=b66105&_nc_ht=video-mxp1-1.xx.fbcdn.net&_nc_ohc=ZQvNX5y0_1gQ7kNvwFWdBMd&efg=eyJ2ZW5jb2RlX3RhZyI6Inhwdl9wcm9ncmVzc2l2ZS5WSV9VU0VDQVNFX1BST0RVQ1RfVFlQRS4uQzMuNzIwLmRhc2hfaDI2NC1iYXNpYy1nZW4yXzcyMHAiLCJ4cHZfYXNzZXRfaWQiOjEyNTgyNDg1NDI3NDA4NjUsImFzc2V0X2FnZV9kYXlzIjozNTksInZpX3VzZWNhc2VfaWQiOjEwMTM5LCJkdXJhdGlvbl9zIjoxMiwidXJsZ2VuX3NvdXJjZSI6Ind3dyJ9&ccb=17-1&vs=70346b3d005c775a&_nc_vs=HBksFQIYRWZiX2VwaGVtZXJhbC9BMTQ4Mzk4RkE3M0IwMkVBNDgzNjRCODA5MDZFNjBCMV9tdF8xX3ZpZGVvX2Rhc2hpbml0Lm1wNBUAAsgBEgAVAhhAZmJfcGVybWFuZW50LzlGNDlFMUVFMDhFNzFEQzAyNEUzQzIyMDI4RDcxOTkxX2F1ZGlvX2Rhc2hpbml0Lm1wNBUCAsgBEgAoABgAGwKIB3VzZV9vaWwBMRJwcm9ncmVzc2l2ZV9yZWNpcGUBMRUAACaChu-W2pe8BBUCKAJDMywXQCgAAAAAAAAYGWRhc2hfaDI2NC1iYXNpYy1nZW4yXzcyMHARAHUAZbaeAQA&_nc_gid=CXhKjE0I-jAPsnY2HWAufw&_nc_ss=7e289&_nc_zt=28&oh=00_AQLzjp0zQpdcDwTblT9m2eHv5WFYDx4hLzEUSMmU_TpnjA&oe=6AB5139D",
      "video_preview_url": "https://scontent-mxp1-1.xx.fbcdn.net/v/t39.35426-6/711695694_2404378190031495_3175246454924053399_n.jpg?_nc_cat=102&ccb=1-7&_nc_sid=c53f8f&_nc_ohc=8n96Gr_0IrEQ7kNvwHWtXgQ&_nc_oc=AdqVInXxe25_YfyFpmh849bsCEOj61j5_c_9sTORV1Zd9ZUVJu5bB12kx7TKh-PcXzo&_nc_zt=14&_nc_ht=scontent-mxp1-1.xx&_nc_gid=CXhKjE0I-jAPsnY2HWAufw&_nc_ss=7e289&oh=00_AQKyNhZtD7LZhNeGGIbt0hnRLCgoPYbTXKH6-TCNdN5L9w&oe=6AB5359C"
    }
  ],
  "first_shown": "2026-06-03",
  "last_shown": "2026-09-19",
  "days_shown": 109,
  "image_url": null,
  "image_urls": [],
  "image_small_url": null,
  "video_url": "https://video-mxp2-1.xx.fbcdn.net/o1/v/t2/f2/m366/AQOMRBFZd7QAA0ruNoYgN6JXjAiwMNc_Yicgl9hZIh8lOlORAPEtR50aqEB4yfQkm6GjRg_8HrdX7-RYLqyVQCzvuyhfySfpOGVKhVn39EzgmQ.mp4?_nc_cat=101&_nc_sid=b66105&_nc_ht=video-mxp2-1.xx.fbcdn.net&_nc_ohc=dEUkWdMHDwYQ7kNvwHiJ8XN&efg=eyJ2ZW5jb2RlX3RhZyI6Inhwdl9wcm9ncmVzc2l2ZS5WSV9VU0VDQVNFX1BST0RVQ1RfVFlQRS4uQzMuNzIwLmRhc2hfaDI2NC1iYXNpYy1nZW4yXzcyMHAiLCJ4cHZfYXNzZXRfaWQiOjgxNTc1MTk4NzU2ODUxNSwiYXNzZXRfYWdlX2RheXMiOjM1OSwidmlfdXNlY2FzZV9pZCI6MTAxMzksImR1cmF0aW9uX3MiOjEyLCJ1cmxnZW5fc291cmNlIjoid3d3In0%3D&ccb=17-1&vs=257c57522b2c310e&_nc_vs=HBksFQIYRWZiX2VwaGVtZXJhbC9CODQzMDVENjk4MkMzOThBMjBFRTg0MzRBMkUyRDg5N19tdF8xX3ZpZGVvX2Rhc2hpbml0Lm1wNBUAAsgBEgAVAhhAZmJfcGVybWFuZW50LzExNEI1QjA2OTQzNEMyQzVCMjVGODRERUJCQjJFQjhEX2F1ZGlvX2Rhc2hpbml0Lm1wNBUCAsgBEgAoABgAGwKIB3VzZV9vaWwBMRJwcm9ncmVzc2l2ZV9yZWNpcGUBMRUAACaG_oz3gfvyAhUCKAJDMywXQCgAAAAAAAAYGWRhc2hfaDI2NC1iYXNpYy1nZW4yXzcyMHARAHUAZbaeAQA&_nc_gid=CXhKjE0I-jAPsnY2HWAufw&_nc_ss=7e289&_nc_zt=28&oh=00_AQKP-fMbUXalHsS9wXs3LMR-_48s_pNZB8-Q4gsPXfn6Nw&oe=6AB52807",
  "video_urls": null,
  "video_duration_s": null,
  "video_preview_url": "https://scontent-mxp1-1.xx.fbcdn.net/v/t39.35426-6/710447423_2716333165389931_221950439945272582_n.jpg?_nc_cat=106&ccb=1-7&_nc_sid=c53f8f&_nc_ohc=Iq_OjuwglZMQ7kNvwFbTh7K&_nc_oc=Adpm8fPUfnukFoj49quE74I_yce8YnDZH9bV7imwtVDPiUtTJFYVW9EdKEwXNHklqYY&_nc_zt=14&_nc_ht=scontent-mxp1-1.xx&_nc_gid=CXhKjE0I-jAPsnY2HWAufw&_nc_ss=7e289&oh=00_AQI43eq2bcH5RMsal09q_k9dBmEqZ4ebUy7tpCOHT7Yc9Q&oe=6AB51813",
  "media_files": null,
  "image_file_url": null,
  "video_file_url": null,
  "transcript": null,
  "transcript_language": null,
  "hook_3s": null,
  "speech_seconds": null,
  "ad_ocr_text": null,
  "sentiment_score": null,
  "analysis_hook": null,
  "analysis_offer": null,
  "analysis_angle": null,
  "analysis_audience": null,
  "analysis_cta_intent": null,
  "analysis_tone": null,
  "analysis_sentiment": null,
  "landing_final_url": null,
  "landing_domain": null,
  "landing_status": null,
  "landing_title": null,
  "landing_is_product": null,
  "landing_product_name": null,
  "landing_price": null,
  "landing_currency": null,
  "platforms": [
    "facebook",
    "instagram",
    "messenger"
  ],
  "launched_last_7_days": null,
  "launched_last_30_days": null,
  "long_running": null,
  "platform_count": null,
  "categories": [
    "unknown"
  ],
  "regions": [],
  "region": "ALL",
  "spend_min": null,
  "spend_max": null,
  "spend_currency": null,
  "impressions_min": null,
  "impressions_max": null,
  "reach_min": null,
  "reach_max": null,
  "paid_for_by": null,
  "funding_entity_website": null,
  "funding_entity_address": null,
  "eu_total_reach": 11900089,
  "eu_reach_breakdown": [
    {
      "country": "HU",
      "age_range": "55-64",
      "male": 9965,
      "female": 9552,
      "unknown": 165
    },
    {
      "country": "HU",
      "age_range": "45-54",
      "male": 28878,
      "female": 15165,
      "unknown": 539
    },
    {
      "country": "HU",
      "age_range": "Unknown",
      "male": 85,
      "female": 15,
      "unknown": 3
    }
  ],
  "eu_audience_age_min": 18,
  "eu_audience_age_max": 65,
  "eu_audience_gender": "All",
  "eu_audience_locations": [
    {
      "name": "Guadeloupe",
      "excluded": false,
      "type": "countries"
    },
    {
      "name": "Aland Islands",
      "excluded": false,
      "type": "countries"
    }
  ],
  "payer": "Shopify",
  "beneficiary": "Shopify",
  "political_reach_by_region": [],
  "political_reach_by_age_gender": [],
  "advertiser_category": "Software",
  "advertiser_about": null,
  "advertiser_categories": null,
  "advertiser_ads_seen": null,
  "advertiser_match": null,
  "advertiser_claimed_ads": null,
  "claimed_count": null,
  "summary_ad_count": null,
  "summary_active_count": null,
  "summary_longest_days": null,
  "summary_formats": null,
  "summary_landing_domains": null,
  "summary_distinct_copies": null,
  "summary_ad_ids": null,
  "advertiser_likes": 4538277,
  "advertiser_ig_username": "shopify",
  "advertiser_ig_followers": 2670355,
  "advertiser_verified": true,
  "advertiser_spend_total": null,
  "advertiser_spend_week": null,
  "advertiser_url": "https://www.facebook.com/shopify/",
  "advertiser_picture_url": "https://scontent.fqrc1-1.fna.fbcdn.net/v/t39.30808-1/532226807_1206379304854792_3907785642549358122_n.jpg?stp=dst-jpg_s148x148_tt6&_nc_cat=1&ccb=1-7&_nc_sid=418b77&_nc_ohc=yojCiHRPRY4Q7kNvwGR-8_b&_nc_oc=Adp5zqArc6xx9hsnNw2-EYHw4eLMMZ49xkrwMX29M0tmUmkCHREDl7smamLE02Cskg4&_nc_zt=24&_nc_ht=scontent.fqrc1-1.fna&_nc_gid=seJgawSzPhkdwcP5u-9nbA&_nc_ss=7e289&oh=00_AQKgCRxis0r2Mjv2KY7tp0qyvceiUdbG2025-XHTVxgm5A&oe=6AB52A47",
  "ad_url": "https://www.facebook.com/ads/library/?id=1657481092030542",
  "matched_query": null,
  "matched_fields": null,
  "first_seen_at": null,
  "last_seen_at": null,
  "ended_at": null,
  "run_tag": null,
  "raw": null
}
```

### Pricing

**Pay only for what a run actually returns**, with no start fee. Prices are per
1,000 and fall with your Apify plan:

| per 1,000 | Free | Starter | Scale | Business |
|---|---|---|---|---|
| Ad creative — copy, link, CTA, media URLs, run dates, platforms | $1.00 | $0.85 | $0.75 | $0.65 |
| EU reach and audience, when the ad reached the EU (`includeEuReach`) | +$0.80 | +$0.70 | +$0.60 | +$0.50 |
| Political spend, impressions and reach, when Meta published them | +$1.00 | +$0.85 | +$0.75 | +$0.65 |
| Advertiser (page) resolved from a name (`resultType: advertisers`) | $2.00 | $1.70 | $1.50 | $1.30 |
| Count row (`resultType: counts`) | $10.00 | $8.50 | $7.50 | $6.50 |
| Creative file copied into your store (`downloadMedia`), per file written | $4.00 | $3.50 | $3.00 | $2.50 |
| Video transcript, when the video has speech (`transcribeVideos`) | +$30.00 | +$26.00 | +$23.00 | +$20.00 |
| Image text read by a vision model, when words were found (`ocrImageAds`), model usage included | +$1.00 | +$1.00 | +$1.00 | +$1.00 |
| Typed analysis — hook, offer, angle, audience, CTA intent, tone, sentiment (`analyzeAds`), model usage included | +$10.00 | +$8.50 | +$7.50 | +$6.50 |
| Landing page resolved and read, when the page answered (`enrichLandingPage`) | +$5.00 | +$4.50 | +$4.00 | +$3.50 |

Each fee is charged **per row that actually carries the thing**. A commercial ad is never
billed the political fee; an ad that never reached the EU is never billed the EU fee; a
repeat the source served twice is never returned, so never billed; a file an earlier run
already stored is linked, not billed again; a music-only video is checked and not billed. `includeAdvertiserInfo` is one request per
advertiser and is not billed separately. Enterprise plans have their own rates — the
Actor's Pricing tab shows the price for your plan.

### Usage patterns

- **Follow a competitor weekly** — Put their page ID in `advertiserIds`, tick `onlyNewAds`, save it as a task and schedule it. Each run returns only creatives the earlier runs did not — the first run delivers the current set, every later run only what appeared since. Connect the task to Slack or email through Apify's integrations and the new ads land where you read.
- **Read what they dropped** — `activeStatus: inactive` returns the ads a page has stopped running. Next to the active set, with `first_shown` and `last_shown`, it is the history of what they tried and for how long.
- **Compare against their Google ads** — Run the same competitor through our Google Ads Transparency Scraper. The two outputs share `advertiser_name`, `headline`, `body_text`, `destination_url`, `cta`, `first_shown`, `last_shown` and `days_shown`, so one sheet shows both platforms.
- **Read what the creatives say** — `transcribeVideos`, `ocrImageAds` and `analyzeAds` together turn a competitor's feed into a table you can pivot: what each video says in its first three seconds, what the artwork promises, and a typed read — angle, offer, audience, CTA intent, tone — per ad. Add `enrichLandingPage` for the page each ad sells, with its price.
- **Measure a political campaign** — `adType: political_and_issue` with a keyword or a page ID, `region` set to the country, and `includeEuReach` for the demographic and regional reach shares. Spend, impressions and reach arrive as the ranges Meta publishes.
- **Size a competitor before you scrape** — `resultType: counts` with page IDs or brand names returns one row per page with the total the Ad Library shows for it (`claimed_count`) — from a single page load, no ads scraped, a cent per row. Run it first to see whether a competitor has 60 ads or 6,000, or schedule it weekly and watch the number move. Ready-made: [Count how many Meta ads each competitor runs](https://apify.com/hyperbach/meta-ads-library-scraper/examples/count-a-competitors-meta-ads).
- **Keep the media** — `image_url` and `video_url` are direct links to Meta's CDN and fetch without a login; they expire about four days after the run. Set `downloadMedia: all` and the files are copied into a key-value store of yours, linked from `media_files`, `image_file_url` and `video_file_url` — no expiry, and a creative met again on a later run is linked, not fetched twice.

### Input configuration

| field | type | default | what it does |
|---|---|---|---|
| `advertiserIds` | `array` | `[]` | Numeric Facebook page IDs, e.g. `20409006880` (Shopify) — the `view_all_page_id` value in an advertiser's Ad Library URL. The whole Ad Library URL is accepted too. Every ad the page runs, across Facebook, Instagram, Messenger, WhatsApp, Audience Network and Threads. |
| `advertiserNames` | `array` | `[]` | Brand or company names, e.g. `Nike`. Each is resolved to the Facebook pages advertising under that name — the pages behind the ads a name search returns, ranked by how well the page name matches — and those pages are then scraped (`resultType: ads`) or listed (`resultType: advertisers`). Use `maxAdvertisersPerName` to control how many pages per name are followed. |
| `searchTerms` | `array` | `[]` | Keywords or brand names, e.g. `shopify`. A keyword search is Meta's ranked sample across every advertiser — the Ad Library returns at most a few hundred creatives for any keyword, whatever total it displays. To get everything one advertiser runs, use its page ID instead. `keywordMatch` chooses loose or exact-phrase matching. |
| `creativeIds` | `array` | `[]` | Library IDs of individual ads, e.g. `1657481092030542` — the `id` in an Ad Library URL (the whole URL is accepted too). One lookup each. |
| `resultType` | `ads` / `advertisers` / `counts` | `"ads"` | `ads` returns one record per ad creative. `advertisers` returns one record per page that `advertiserNames` or `searchTerms` resolve to — page id, name, categories, likes, how many matching ads it runs — so you can see which page a brand advertises under before pulling its ads (the same idiom as our Google Ads Transparency Scraper). `counts` returns one record per target with the total the Ad Library shows for it, from a single page load and without scraping any ad. |
| `maxAdvertisersPerName` | `integer` | `3` | How many resolved pages to follow for each entry in `advertiserNames`. A common name can match several pages (regional pages, resellers); the best name match ranks first. |
| `startUrls` | `array` | `[]` | Paste Ad Library URLs — an advertiser page (`...?view_all_page_id=...`), a keyword search (`...?q=...`) or a single ad (`...?id=...`). A Facebook page URL (`facebook.com/shopify`) works too: the Actor finds the advertiser whose page name is that name, and returns nothing rather than a different page when no name matches. A `country` in the URL is used unless you set `region` explicitly. |
| `region` | `string` |  | Two-letter country code for where the ads were shown, e.g. `US`, `GB` (`UK` also works), `DE`, or `ALL` for every country at once — the same `country=` value the Ad Library puts in its URLs. Leave it empty and the country in a pasted `startUrls` URL is used, or `ALL` when there is none. The EU transparency figures (`includeEuReach`) do not depend on this: they come back for any ad that reached the EU whatever country you ask for. |
| `regions` | `array` | `[]` | Several country codes in one run, e.g. `["DE", "FR", "IT"]` — ads shown in any of them. Wins over `region` when given; `ALL` in the list means every country. Rows carry the list in `region`. |
| `targetedCountryOnly` | `boolean` | `false` | With a country in `region`/`regions`: ask the Ad Library for ads whose advertiser targeted that country, not every ad that happened to be shown there — its own 'targeted' switch, passed through as is. Measured 2026-09-20 on a page and on a keyword search in DE: the first 30 rows were identical with and without it, so expect a small difference at most. No effect with `ALL`. |
| `activeStatus` | `active` / `inactive` / `all` | `"active"` | `active` returns ads running now, `inactive` ads that have stopped, `all` both. Inactive ads are how you read a competitor's history: what they tried and dropped. |
| `adType` | `all` / `political_and_issue` / `housing` / `employment` / `financial_products` | `"all"` | `all` for every ad. `political_and_issue` narrows to ads about social issues, elections or politics — the only ads Meta publishes spend, impressions and reach for; those arrive as ranges in `spend_min`/`spend_max`, `impressions_min`/`impressions_max` and `reach_min`/`reach_max`, with the funding entity in `paid_for_by`. `housing`, `employment` and `financial_products` are Meta's special ad categories. |
| `mediaType` | `all` / `image` / `video` / `meme` / `image_and_meme` / `none` | `"all"` | Keep only creatives of this media type, as the Ad Library's own filter defines them. `all` is every type. |
| `platforms` | `array` | `[]` | Keep only ads that ran on these Meta platforms. Empty means all. Every row also lists where it ran in `platforms`. |
| `languages` | `array` | `[]` | Keep only creatives in these languages, as two-letter ISO 639-1 codes, e.g. `["es", "pt"]` — the Ad Library's own language filter. Empty means all. |
| `sortBy` | `auto` / `relevance` / `most_recent` / `most_impressions` / `least_impressions` | `"auto"` | `auto` picks the order that reaches the most: for an advertiser the newest-first order walks every creative in a straight line (measured: relevance starts repeating after ~1,240), for a search term relevance yields more. `most_impressions` / `least_impressions` order by Meta's impressions bucket, which exists on ads about social issues, elections or politics only. |
| `expandVariants` | `boolean` | `true` | The Library groups ads that share a creative and text ("N ads use this creative") and shows one per group; its result count counts them all. On, each group is opened and every ad in it is a row of its own — the count the Library shows, the way the lane's biggest actors deliver it (Shopify: 1,753 creatives, 5,597 ads). Off, one row per creative with `collation_count` saying how many ads use it: fewer rows, fewer fees, the creative-level view. |
| `keywordMatch` | `unordered` / `exact_phrase` | `"unordered"` | How `searchTerms` match ad text: `unordered` finds ads containing the words in any order, `exact_phrase` only the phrase as typed. A pasted search URL keeps its own `search_type`. |
| `keywordRelevance` | `any` / `visible` | `"any"` | A keyword search is the Ad Library's loose match: `shopify` also returns ads that mention the word in passing. Every keyword row carries `matched_fields` — which of its texts (headline, body, cards, link, advertiser name) contain the whole term. `visible` keeps only rows with at least one such field; `any` keeps everything the Library returned. |
| `dedupeBy` | `none` / `advertiser` | `"none"` | `advertiser` keeps one ad per advertiser in a keyword search — a who-is-advertising-on-this read instead of every creative. `none` keeps all. |
| `startDate` | `string` |  | `YYYY-MM-DD`. Keeps ads whose run overlaps this date or later — an ad that started years ago and is still running counts, because it is still running. |
| `endDate` | `string` |  | `YYYY-MM-DD`. Keeps ads whose run overlaps this date or earlier. |
| `period` | \`\` / `last24h` / `last7d` / `last14d` / `last30d` / `last90d` | `""` | Keep only creatives that started running in the window: the last 24 hours, 7, 14, 30 or 90 days. A preset over `first_shown`; combine with `sortBy: most_recent` for a 'what did they launch this week' read. Empty means no window. |
| `longRunningDays` | `integer` | `30` | An ad shown for at least this many days gets `long_running: true` — the fastest read on which creatives earn their budget. Default 30. |
| `includeEuReach` | `boolean` | `false` | One extra request per ad that adds what the Ad Library publishes under the EU Digital Services Act: total EU reach, the reach breakdown by country, age and gender, the audience the advertiser chose (age range, gender, included and excluded locations) and the payer and beneficiary. Present on ads that reached the EU, null on the rest; billed only on rows that carry a figure. Also fills the advertiser columns below. |
| `includeAdvertiserInfo` | `boolean` | `false` | One extra request per advertiser (not per ad) that fills `advertiser_category`, `advertiser_about`, `advertiser_likes`, `advertiser_ig_username`, `advertiser_ig_followers`, `advertiser_verified` and, for political pages, lifetime and last-week spend. Not billed separately. |
| `downloadMedia` | `none` / `images` / `all` | `"none"` | Copy each creative's files from Meta's CDN into a key-value store you own, so the links do not expire with Meta's ~4-day URLs. Every row then carries `media_files` (store URL, size, type per file) plus `image_file_url` / `video_file_url`. Files are keyed by creative id, so a creative met again on a later run is linked to its existing file and not fetched or charged twice. Billed per file written. |
| `mediaStoreName` | `string` |  | Named key-value store the files go into. Optional: left empty, the name is derived from the targets (`meta-ads-media-…`), so a scheduled task keeps adding to the same store. Set one to collect several inputs into one place, e.g. `creatives-shopify`. |
| `transcribeVideos` | `boolean` | `false` | Turn the speech in every video creative into text: `transcript`, `transcript_language` (ISO code), `hook_3s` (the words spoken in the first three seconds of speech) and `speech_seconds`. A music-only or silent video is checked and comes back with `speech_seconds: 0` — and is not billed. Billed per row that carries a transcript. |
| `ocrImageAds` | `boolean` | `false` | Read the words baked into the artwork with a vision model — the headline set over the photo, the offer sticker, the price — into `ad_ocr_text`. The image read is the creative's first image, or the poster frame of a video ad. Read once per creative and cached. Billed per row where words were found; model usage is included. |
| `analyzeAds` | `boolean` | `false` | One structured read per creative by an OpenAI model: `analysis_hook` (the scroll-stopper), `analysis_offer`, `analysis_angle` (problem\_solution, social\_proof, scarcity, discount, feature, lifestyle, curiosity, education, comparison, brand, other), `analysis_audience`, `analysis_cta_intent`, `analysis_tone` and `analysis_sentiment` (-1..1). Reads the copy, the cards, the CTA, and the image text and transcript when those switches are on. A free `sentiment_score` (AFINN word list) is on every row regardless. Billed per row analysed; model usage included. |
| `enrichLandingPage` | `boolean` | `false` | Follow each ad's link to the page it lands on: `landing_final_url` (redirects followed, tracking parameters stripped), `landing_domain`, `landing_status`, `landing_title`, and — from the page's own product markup (Open Graph, JSON-LD) — `landing_is_product`, `landing_product_name`, `landing_price`, `landing_currency`. One fetch per distinct URL per run. Billed per row whose page answered. |
| `openaiApiKey` | `string` |  | Optional. The Actor uses its own model access by default and bills a flat fee per transcript, image read or analysis. Supply your own OpenAI key only if you would rather the model usage went to your account. |
| `summaryRows` | `boolean` | `false` | After the ads, add one `advertiser_profile` row per advertiser (ad count, active count, longest-running days, formats, landing domains, distinct copies, the ad ids) and one `landing_page` row per destination URL with the ads pointing at it. Computed from the rows already delivered — no extra requests, not billed. |
| `runTag` | `string` |  | Your own label for this run — a client name, a campaign, a week — copied into `run_tag` on every row, so rows from several runs sort in one sheet or table. |
| `maxAds` | `integer` | `0` | Stop after this many ads in total, across every search. 0 means no limit. |
| `maxAdsPerTarget` | `integer` | `0` | Cap each page ID, keyword or URL separately. Without this, one prolific advertiser can use the whole `maxAds` budget before the next target starts. 0 means no per-target cap. |
| `onlyNewAds` | `boolean` | `false` | Skip creatives delivered by previous runs of the same targets (or of the same `stateStoreName`). Skipped ads are never returned and never charged — a weekly monitoring run pays only for what actually changed. Every row also carries `first_seen_at` / `last_seen_at` from the same memory. |
| `includeEndedAds` | `boolean` | `false` | With a monitoring memory (the same store `onlyNewAds` uses): after a complete scan of a target, add one `ended` row for every creative earlier runs saw there that the Library no longer returns — with when it was first and last seen. A capped or cut-off scan reports none, because it cannot vouch for an absence. Not billed. |
| `stateStoreName` | `string` |  | Name of the memory that holds the creative IDs already delivered, so `onlyNewAds` can tell new from old across runs. The memory is kept by the Actor on its side, one per Apify account — nothing is stored in your account. Optional: left empty, the name is derived from the targets, so a scheduled task remembers on its own. Set one to share memory between different inputs, e.g. `ads-shopify`, or change it to start over. |
| `resumeFromRunId` | `string` |  | The id of an earlier run of this Actor that timed out or was aborted, with the same input: this run continues from its checkpoint — finished targets skipped, the target in progress continued from its cursor, no creative delivered twice, `maxAds` counted across both. A run that stops on its own can also simply be resurrected (Apify's Resurrect): it finds its own checkpoint. |
| `requestDelaySecs` | `number` | `0.3` | Seconds to wait between result pages. The source paces itself by latency; a small delay keeps one exit useful for longer. |
| `concurrency` | `integer` | `3` | How many advertisers or search terms are paged at the same time, each on its own residential exit (the details and variant calls run on a separate pool of three). 3 is measured safe; 5 for a long list of small targets. |
| `includeRawData` | `boolean` | `false` | Add Meta's own untouched record to each row as `raw`, for callers who would rather map the fields themselves. |

### FAQ

**The Ad Library says a page has ~5,700 ads. Will I get them all?**

Nearly. The count is ads; the list shows one per creative group, and this actor opens every group (`expandVariants`, on by default). On Shopify, measured 2026-09-20: 2,310 creatives opened into 5,337 ads, 95% of the 5,597 the two most-used actors deliver, in 19 minutes. The source refuses cursors past roughly 1,900 creatives in any one order; when a walk hits that, the actor continues in media-type and platform slices and the status names what was cut. A keyword search is different: it is Meta's ranked sample, a few hundred creatives whatever the count says, on every actor.

**Why are `spend_min`, `impressions_max` and `reach_min` null on my ads?**

Meta publishes spend, impressions and reach only for ads about social issues, elections or politics. Commercial ads never carry them, on the Ad Library site or anywhere else — every actor in this category returns null there, whatever its schema promises. Use `adType: political_and_issue` for the ads that have them. For commercial ads the one reach figure that exists is the EU one: switch on `includeEuReach`.

**What does `includeEuReach` cost me?**

One extra request per ad, and the per-row EU fee only on rows that came back with a figure — an ad that never reached the EU is not billed for it. `includeAdvertiserInfo` alone is one request per advertiser and is not billed separately.

**Can I search by a brand name instead of a page ID?**

Yes — put it in `searchTerms`. A keyword search is Meta's ranked sample across every advertiser mentioning the words, which is right for a topic and wrong for a page: it caps at a few hundred creatives and mixes advertisers. To get everything one page runs, open it in the Ad Library and copy the `view_all_page_id` from the URL into `advertiserIds`, or paste the URL into `startUrls`.

**Which `region` should I use?**

`ALL` returns every ad the page runs anywhere. A country code narrows to ads shown there. The EU figures do not depend on it: `includeEuReach` returns them for any ad that reached the EU, whatever country you asked for.

**The image or video link stopped working.**

Meta's CDN links carry an expiry of about four days from the moment the run fetched them. Download what you need within that window; a new run issues fresh links.

**What does a PARTIAL status mean?**

The source refused to keep answering even after the actor rotated through fresh exits, or answered with errors on every attempt, for the searches named in the status. Whatever was delivered is in the dataset; the named searches are incomplete. It is rare, and it is ours to fix — a PARTIAL run is a bug report, not a configuration problem on your side.

### Integration

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('hyperbach/meta-ads-library-scraper').call({"advertiserIds": ["20409006880"], "region": "ALL", "maxAds": 100});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

#### Python

```python
from apify_client import ApifyClient
client = ApifyClient('YOUR_TOKEN')
run = client.actor('hyperbach/meta-ads-library-scraper').call(run_input={'advertiserIds': ['20409006880'], 'region': 'ALL', 'maxAds': 100})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

#### CLI

```bash
apify call hyperbach/meta-ads-library-scraper --input '{"advertiserIds": ["20409006880"], "region": "ALL", "maxAds": 100}'
```

#### REST

```bash
curl -X POST "https://api.apify.com/v2/acts/hyperbach~meta-ads-library-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' -d '{"advertiserIds": ["20409006880"], "region": "ALL", "maxAds": 100}'
```

### Support

apify@hyperbach.com

*This page is generated from the Actor's schemas and a live sample — it cannot describe a field the Actor does not have.*

# Actor input Schema

## `advertiserIds` (type: `array`):

Numeric Facebook page IDs, e.g. `20409006880` (Shopify) — the `view_all_page_id` value in an advertiser's Ad Library URL. The whole Ad Library URL is accepted too. Every ad the page runs, across Facebook, Instagram, Messenger, WhatsApp, Audience Network and Threads.

## `advertiserNames` (type: `array`):

Brand or company names, e.g. `Nike`. Each is resolved to the Facebook pages advertising under that name — the pages behind the ads a name search returns, ranked by how well the page name matches — and those pages are then scraped (`resultType: ads`) or listed (`resultType: advertisers`). Use `maxAdvertisersPerName` to control how many pages per name are followed.

## `searchTerms` (type: `array`):

Keywords or brand names, e.g. `shopify`. A keyword search is Meta's ranked sample across every advertiser — the Ad Library returns at most a few hundred creatives for any keyword, whatever total it displays. To get everything one advertiser runs, use its page ID instead. `keywordMatch` chooses loose or exact-phrase matching.

## `creativeIds` (type: `array`):

Library IDs of individual ads, e.g. `1657481092030542` — the `id` in an Ad Library URL (the whole URL is accepted too). One lookup each.

## `resultType` (type: `string`):

`ads` returns one record per ad creative. `advertisers` returns one record per page that `advertiserNames` or `searchTerms` resolve to — page id, name, categories, likes, how many matching ads it runs — so you can see which page a brand advertises under before pulling its ads (the same idiom as our Google Ads Transparency Scraper). `counts` returns one record per target with the total the Ad Library shows for it, from a single page load and without scraping any ad.

## `maxAdvertisersPerName` (type: `integer`):

How many resolved pages to follow for each entry in `advertiserNames`. A common name can match several pages (regional pages, resellers); the best name match ranks first.

## `startUrls` (type: `array`):

Paste Ad Library URLs — an advertiser page (`...?view_all_page_id=...`), a keyword search (`...?q=...`) or a single ad (`...?id=...`). A Facebook page URL (`facebook.com/shopify`) works too: the Actor finds the advertiser whose page name is that name, and returns nothing rather than a different page when no name matches. A `country` in the URL is used unless you set `region` explicitly.

## `region` (type: `string`):

Two-letter country code for where the ads were shown, e.g. `US`, `GB` (`UK` also works), `DE`, or `ALL` for every country at once — the same `country=` value the Ad Library puts in its URLs. Leave it empty and the country in a pasted `startUrls` URL is used, or `ALL` when there is none. The EU transparency figures (`includeEuReach`) do not depend on this: they come back for any ad that reached the EU whatever country you ask for.

## `regions` (type: `array`):

Several country codes in one run, e.g. `["DE", "FR", "IT"]` — ads shown in any of them. Wins over `region` when given; `ALL` in the list means every country. Rows carry the list in `region`.

## `targetedCountryOnly` (type: `boolean`):

With a country in `region`/`regions`: ask the Ad Library for ads whose advertiser targeted that country, not every ad that happened to be shown there — its own 'targeted' switch, passed through as is. Measured 2026-09-20 on a page and on a keyword search in DE: the first 30 rows were identical with and without it, so expect a small difference at most. No effect with `ALL`.

## `activeStatus` (type: `string`):

`active` returns ads running now, `inactive` ads that have stopped, `all` both. Inactive ads are how you read a competitor's history: what they tried and dropped.

## `adType` (type: `string`):

`all` for every ad. `political_and_issue` narrows to ads about social issues, elections or politics — the only ads Meta publishes spend, impressions and reach for; those arrive as ranges in `spend_min`/`spend_max`, `impressions_min`/`impressions_max` and `reach_min`/`reach_max`, with the funding entity in `paid_for_by`. `housing`, `employment` and `financial_products` are Meta's special ad categories.

## `mediaType` (type: `string`):

Keep only creatives of this media type, as the Ad Library's own filter defines them. `all` is every type.

## `platforms` (type: `array`):

Keep only ads that ran on these Meta platforms. Empty means all. Every row also lists where it ran in `platforms`.

## `languages` (type: `array`):

Keep only creatives in these languages, as two-letter ISO 639-1 codes, e.g. `["es", "pt"]` — the Ad Library's own language filter. Empty means all.

## `sortBy` (type: `string`):

`auto` picks the order that reaches the most: for an advertiser the newest-first order walks every creative in a straight line (measured: relevance starts repeating after ~1,240), for a search term relevance yields more. `most_impressions` / `least_impressions` order by Meta's impressions bucket, which exists on ads about social issues, elections or politics only.

## `expandVariants` (type: `boolean`):

The Library groups ads that share a creative and text ("N ads use this creative") and shows one per group; its result count counts them all. On, each group is opened and every ad in it is a row of its own — the count the Library shows, the way the lane's biggest actors deliver it (Shopify: 1,753 creatives, 5,597 ads). Off, one row per creative with `collation_count` saying how many ads use it: fewer rows, fewer fees, the creative-level view.

## `keywordMatch` (type: `string`):

How `searchTerms` match ad text: `unordered` finds ads containing the words in any order, `exact_phrase` only the phrase as typed. A pasted search URL keeps its own `search_type`.

## `keywordRelevance` (type: `string`):

A keyword search is the Ad Library's loose match: `shopify` also returns ads that mention the word in passing. Every keyword row carries `matched_fields` — which of its texts (headline, body, cards, link, advertiser name) contain the whole term. `visible` keeps only rows with at least one such field; `any` keeps everything the Library returned.

## `dedupeBy` (type: `string`):

`advertiser` keeps one ad per advertiser in a keyword search — a who-is-advertising-on-this read instead of every creative. `none` keeps all.

## `startDate` (type: `string`):

`YYYY-MM-DD`. Keeps ads whose run overlaps this date or later — an ad that started years ago and is still running counts, because it is still running.

## `endDate` (type: `string`):

`YYYY-MM-DD`. Keeps ads whose run overlaps this date or earlier.

## `period` (type: `string`):

Keep only creatives that started running in the window: the last 24 hours, 7, 14, 30 or 90 days. A preset over `first_shown`; combine with `sortBy: most_recent` for a 'what did they launch this week' read. Empty means no window.

## `longRunningDays` (type: `integer`):

An ad shown for at least this many days gets `long_running: true` — the fastest read on which creatives earn their budget. Default 30.

## `includeEuReach` (type: `boolean`):

One extra request per ad that adds what the Ad Library publishes under the EU Digital Services Act: total EU reach, the reach breakdown by country, age and gender, the audience the advertiser chose (age range, gender, included and excluded locations) and the payer and beneficiary. Present on ads that reached the EU, null on the rest; billed only on rows that carry a figure. Also fills the advertiser columns below.

## `includeAdvertiserInfo` (type: `boolean`):

One extra request per advertiser (not per ad) that fills `advertiser_category`, `advertiser_about`, `advertiser_likes`, `advertiser_ig_username`, `advertiser_ig_followers`, `advertiser_verified` and, for political pages, lifetime and last-week spend. Not billed separately.

## `downloadMedia` (type: `string`):

Copy each creative's files from Meta's CDN into a key-value store you own, so the links do not expire with Meta's ~4-day URLs. Every row then carries `media_files` (store URL, size, type per file) plus `image_file_url` / `video_file_url`. Files are keyed by creative id, so a creative met again on a later run is linked to its existing file and not fetched or charged twice. Billed per file written.

## `mediaStoreName` (type: `string`):

Named key-value store the files go into. Optional: left empty, the name is derived from the targets (`meta-ads-media-…`), so a scheduled task keeps adding to the same store. Set one to collect several inputs into one place, e.g. `creatives-shopify`.

## `transcribeVideos` (type: `boolean`):

Turn the speech in every video creative into text: `transcript`, `transcript_language` (ISO code), `hook_3s` (the words spoken in the first three seconds of speech) and `speech_seconds`. A music-only or silent video is checked and comes back with `speech_seconds: 0` — and is not billed. Billed per row that carries a transcript.

## `ocrImageAds` (type: `boolean`):

Read the words baked into the artwork with a vision model — the headline set over the photo, the offer sticker, the price — into `ad_ocr_text`. The image read is the creative's first image, or the poster frame of a video ad. Read once per creative and cached. Billed per row where words were found; model usage is included.

## `analyzeAds` (type: `boolean`):

One structured read per creative by an OpenAI model: `analysis_hook` (the scroll-stopper), `analysis_offer`, `analysis_angle` (problem\_solution, social\_proof, scarcity, discount, feature, lifestyle, curiosity, education, comparison, brand, other), `analysis_audience`, `analysis_cta_intent`, `analysis_tone` and `analysis_sentiment` (-1..1). Reads the copy, the cards, the CTA, and the image text and transcript when those switches are on. A free `sentiment_score` (AFINN word list) is on every row regardless. Billed per row analysed; model usage included.

## `enrichLandingPage` (type: `boolean`):

Follow each ad's link to the page it lands on: `landing_final_url` (redirects followed, tracking parameters stripped), `landing_domain`, `landing_status`, `landing_title`, and — from the page's own product markup (Open Graph, JSON-LD) — `landing_is_product`, `landing_product_name`, `landing_price`, `landing_currency`. One fetch per distinct URL per run. Billed per row whose page answered.

## `openaiApiKey` (type: `string`):

Optional. The Actor uses its own model access by default and bills a flat fee per transcript, image read or analysis. Supply your own OpenAI key only if you would rather the model usage went to your account.

## `summaryRows` (type: `boolean`):

After the ads, add one `advertiser_profile` row per advertiser (ad count, active count, longest-running days, formats, landing domains, distinct copies, the ad ids) and one `landing_page` row per destination URL with the ads pointing at it. Computed from the rows already delivered — no extra requests, not billed.

## `runTag` (type: `string`):

Your own label for this run — a client name, a campaign, a week — copied into `run_tag` on every row, so rows from several runs sort in one sheet or table.

## `maxAds` (type: `integer`):

Stop after this many ads in total, across every search. 0 means no limit.

## `maxAdsPerTarget` (type: `integer`):

Cap each page ID, keyword or URL separately. Without this, one prolific advertiser can use the whole `maxAds` budget before the next target starts. 0 means no per-target cap.

## `onlyNewAds` (type: `boolean`):

Skip creatives delivered by previous runs of the same targets (or of the same `stateStoreName`). Skipped ads are never returned and never charged — a weekly monitoring run pays only for what actually changed. Every row also carries `first_seen_at` / `last_seen_at` from the same memory.

## `includeEndedAds` (type: `boolean`):

With a monitoring memory (the same store `onlyNewAds` uses): after a complete scan of a target, add one `ended` row for every creative earlier runs saw there that the Library no longer returns — with when it was first and last seen. A capped or cut-off scan reports none, because it cannot vouch for an absence. Not billed.

## `stateStoreName` (type: `string`):

Name of the memory that holds the creative IDs already delivered, so `onlyNewAds` can tell new from old across runs. The memory is kept by the Actor on its side, one per Apify account — nothing is stored in your account. Optional: left empty, the name is derived from the targets, so a scheduled task remembers on its own. Set one to share memory between different inputs, e.g. `ads-shopify`, or change it to start over.

## `resumeFromRunId` (type: `string`):

The id of an earlier run of this Actor that timed out or was aborted, with the same input: this run continues from its checkpoint — finished targets skipped, the target in progress continued from its cursor, no creative delivered twice, `maxAds` counted across both. A run that stops on its own can also simply be resurrected (Apify's Resurrect): it finds its own checkpoint.

## `requestDelaySecs` (type: `number`):

Seconds to wait between result pages. The source paces itself by latency; a small delay keeps one exit useful for longer.

## `concurrency` (type: `integer`):

How many advertisers or search terms are paged at the same time, each on its own residential exit (the details and variant calls run on a separate pool of three). 3 is measured safe; 5 for a long list of small targets.

## `includeRawData` (type: `boolean`):

Add Meta's own untouched record to each row as `raw`, for callers who would rather map the fields themselves.

## Actor input object example

```json
{
  "advertiserIds": [
    "20409006880"
  ],
  "advertiserNames": [],
  "searchTerms": [],
  "creativeIds": [],
  "resultType": "ads",
  "maxAdvertisersPerName": 3,
  "startUrls": [],
  "regions": [],
  "targetedCountryOnly": false,
  "activeStatus": "active",
  "adType": "all",
  "mediaType": "all",
  "platforms": [],
  "languages": [],
  "sortBy": "auto",
  "expandVariants": true,
  "keywordMatch": "unordered",
  "keywordRelevance": "any",
  "dedupeBy": "none",
  "period": "",
  "longRunningDays": 30,
  "includeEuReach": false,
  "includeAdvertiserInfo": false,
  "downloadMedia": "none",
  "transcribeVideos": false,
  "ocrImageAds": false,
  "analyzeAds": false,
  "enrichLandingPage": false,
  "summaryRows": false,
  "maxAds": 50,
  "maxAdsPerTarget": 0,
  "onlyNewAds": false,
  "includeEndedAds": false,
  "requestDelaySecs": 0.3,
  "concurrency": 3,
  "includeRawData": false
}
```

# Actor output Schema

## `results` (type: `string`):

All scraped records in the default dataset. One record per ad creative:

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "advertiserIds": [
        "20409006880"
    ],
    "maxAds": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("hyperbach/meta-ads-library-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "advertiserIds": ["20409006880"],
    "maxAds": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("hyperbach/meta-ads-library-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "advertiserIds": [
    "20409006880"
  ],
  "maxAds": 50
}' |
apify call hyperbach/meta-ads-library-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hyperbach/meta-ads-library-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/uHm94BMyxm1koOhQg/builds/E0nl0CKchUIc1zHMy/openapi.json
