# Facebook Ads Library Scraper — Meta & Instagram (`chronometrica/facebook-ads-scraper`) Actor

Scrape Facebook Ad Library and Meta ads by advertiser, keyword, or URL. Get ad copy, images, videos, carousel cards, links, dates, and optional EU data. Export JSON, CSV, or Excel for competitor ad research. Pay only for successful saved ads.

- **URL**: https://apify.com/chronometrica/facebook-ads-scraper.md
- **Developed by:** [Chronometrica](https://apify.com/chronometrica) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 successful ads

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Facebook Ads Library Scraper — Meta & Instagram

### 📊 What does Facebook Ads Library Scraper do?

Search public Facebook and Instagram ads from Meta Ad Library, also called Facebook Ad Library. Enter a Facebook page, keyword, or Ad Library URL. Get ad copy, images, videos, carousel cards, landing page links, dates, and advertiser details in one row per ad. Export the results to JSON, CSV, or Excel.

Compare competitor offers and ad copy. Build a creative library or schedule repeat searches to track competitor ads. A carousel card is one item in a multi-card ad. Each card keeps its text, links, and media together. Partnership ads keep the advertiser separate from the page that published the creative.

No Facebook login, supplied account cookies, or Meta API plan is required. The Actor collects public library data. It does not collect private Ads Manager performance, conversions, or return on ad spend.

**Pay only for successful ads saved to your dataset.** Creative cards and optional public details have no extra fee.

With Facebook Ads Library Scraper, you can:

- 🔎 Find competitor ads by Facebook page, keyword, or Ad Library URL.
- 🖼️ Save ad copy, carousel cards, image and video URLs, and preview images.
- 🔗 Keep each card's copy, call to action, and destination together.
- 🤝 Keep the advertiser and creative publisher separate on partnership ads.
- 🌍 Filter by search country, active status, ad category, media, platform, language, and dates.
- 🧾 Add public advertiser profiles and EU audience or payer data when available.
- ♻️ Save each ad once per run, with the inputs that found it in `matches`.
- 📦 Export JSON, JSONL, CSV, Excel, XML, RSS, or HTML through Apify.

### 📦 What data can I extract?

**One dataset row represents the first retained observation of one ad archive ID during one run.** Its key is (`runId`, `adArchiveId`). A later match adds a relationship to the same row. It does not replace the first ad facts. Cards, media versions, and input matches stay inside that row.

The JSON fields follow this order:

| Data group             | Fields and use                                                                             |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| 🆔 Observation         | `runId`, `adArchiveId`, `observedAt`, `sourceAdId`, `adUrl`                                |
| 🏢 Advertiser          | Page identity, profile image, categories, deleted flag, likes, and `advertiserDetails`     |
| 🤝 Creative publisher  | `creativePage` keeps the source creative page identity and its own page measurements       |
| 📅 Ad context          | Active flag, source dates, platforms, countries, categories, collation, and EU eligibility |
| 🖼️ Content             | Format, copy, links, ordered `creatives`, extra variants, media lists, and content flags   |
| 🧾 Disclosures         | Source currency, spend, reach, impressions, and EU audience data under `disclosures`       |
| 📌 Input relationships | `matches` records the processed inputs that returned the ad                                |
| 🚦 Quality             | `status`, `statusReason`, and `fieldStates`                                                |

This is the exported JSON model. The Actor does not create separate database tables.
Missing values remain `null`. An explicit source zero stays zero. An explicit empty source list stays `[]`.

For dynamic product ads, the ad-level title can contain `{{product.name}}`. That is source text. Use each entry in `creatives` for its product title, link, and media. The Actor does not generate replacement copy.

### ⚙️ Can I use this as a Meta Ad Library API?

Yes. Use the Apify API to search public Facebook and Instagram ads and return structured data. This is an independent scraper API. It is not Meta’s official Ad Library API. You can also use Apify schedules, webhooks, and integrations. JSON keeps each ad’s creative cards and media together. CSV and Excel are useful for flat ad lists.

Example with the Apify JavaScript client:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor("chronometrica/facebook-ads-scraper").call({
  targets: ["15087023444"],
  countries: ["US"],
  activeStatus: "active",
  maxAds: 10,
  details: "none",
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

### 🎯 Why scrape Facebook Ad Library and Meta ads?

| Use case                | How the data helps                                                 |
| ----------------------- | ------------------------------------------------------------------ |
| 🕵️ Competitor research  | Compare public offers, messages, formats, and landing pages        |
| 🖼️ Creative libraries   | Store ordered cards and media links with stable ad IDs             |
| 🌍 Regional research    | Compare library results under country and platform filters         |
| 🤝 Partnership research | Keep a sponsoring advertiser separate from a creator page          |
| 📅 Ad comparisons       | Repeat inputs and compare observations by ad ID                    |
| 🧱 Data pipelines       | Load a fixed output shape into your database or reporting workflow |

Repeated runs provide observations. They do not automatically compute change reports. An ad missing from a filtered, capped, or incomplete run does not prove that it stopped running. To build a latest view, select the latest eligible observation for each ad ID. Keep earlier observations when you need history.

### 💵 Pricing

You pay only for a successful unique ad saved to your dataset. The custom event is `successful-ad`.

| Your Apify plan | Store discount tier | Price per 1,000 successful ads |
| --------------- | ------------------- | -----------------------------: |
| Free            | FREE                |                      **$2.75** |
| Starter         | BRONZE              |                      **$2.50** |
| Scale           | SILVER              |                      **$2.25** |
| Business        | GOLD                |                      **$2.00** |
| Enterprise      | PLATINUM / DIAMOND  |                      **$2.00** |

There is **no start fee**, subscription fee, or separate details fee. Standard-run platform and proxy usage are included. Apify account plan fees are separate. These are plan discounts, not volume bands within one run.

A successful ad has a valid archive ID, passes the output and creative checks, has usable creative text, a link, or media, and has row status `ok`. Missing optional source values, such as undisclosed spend, do not make an otherwise valid ad fail. Source product templates remain unchanged; use `creatives[].title` for product titles.

| Result                                                                          | Actor event charge                     |
| ------------------------------------------------------------------------------- | -------------------------------------- |
| Successful unique ad saved                                                      | One `successful-ad` event              |
| Saved `partial` ad or empty creative record                                     | Free                                   |
| Invalid input, failed lookup, blocked request, filtered candidate, or duplicate | Free                                   |
| Empty search or count-only run                                                  | Free                                   |
| Valid ads saved before a later failure, time limit, or spending stop            | Charge only those successful saved ads |

Cards, media versions, input matches, and optional public details inside an ad do not add charges. A later run creates new observations and can charge for successful ads again.

At the Free-plan rate, the Actor event charge is:

`successful unique ads × $0.00275`

| Successful ads | Free-plan event charge |
| -------------: | ---------------------: |
|              0 |                     $0 |
|             10 |                $0.0275 |
|            100 |                $0.2750 |
|          1,000 |                $2.7500 |

The Console requires a spending allowance of at least $0.01. This is a spending limit, not a minimum charge. A run with no successful ads has no Actor event charge.

The Actor allows 256 MB to 1 GB memory. Keep the default 1 GB for your first run. The default run time limit is 20 minutes. Bulk public details take longer than basic collection. Use `maxAds` and the Console spending limit to control result volume. `maxAds` caps all saved ad rows, including free partial rows. It does not guarantee that many results.

### 🚀 How do I use Facebook Ads Library Scraper?

1. Open the Actor in Apify Console.
2. In **Pages, Ad Library URLs, or keywords**, replace the example with your page URL, Library URL, page ID, or keywords. Each entry starts a separate search.
3. Set **Total ad limit — all inputs** to `10` for your first run.
4. Leave **Countries** blank to use each Library URL's country, or United States when the input has no country. Select countries only when you want an explicit choice.
5. Optional sections hold filters and sorting, ad start dates, activity windows, and output choices. **Standard ad data** already includes creative cards and media links.
6. Click **Start**.
7. Open **Output → Ads**. Use **Table** or **JSON** to inspect the data.
8. Use **Export** to download results. Select **Run summary and input outcomes** to check collection limits and errors.

### ⬇️ Input

Use the tested small example:

```json
{
  "targets": ["https://www.facebook.com/nike/"],
  "maxAds": 10
}
```

#### 🔎 Target types

| Input                    | Example                                                                         | Meaning                                             |
| ------------------------ | ------------------------------------------------------------------------------- | --------------------------------------------------- |
| Advertiser page ID       | `15087023444`                                                                   | List public ads for that advertiser                 |
| Public Facebook page URL | `https://www.facebook.com/nike/`                                                | Resolve its library advertiser ID                   |
| Advertiser Library URL   | `https://www.facebook.com/ads/library/?view_all_page_id=15087023444&country=US` | Use the advertiser and supported URL filters        |
| Keyword Library URL      | `https://www.facebook.com/ads/library/?q=running%20shoes&country=US`            | Use the keyword and supported URL filters           |
| Single-ad URL            | `https://www.facebook.com/ads/library/?id=1829888761376507`                     | Collect that exact public archive ad when available |

A bare numeric ID means an advertiser page ID. Use a Library URL with `id=` for a single ad. A modern Facebook profile ID can differ from its library advertiser ID; a supported page URL lets the Actor resolve that difference.

Plain text in `targets` is a keyword search. For example, `"running shoes"` searches across advertisers. A public Facebook URL can omit `https://`; unsupported website URLs are rejected. Replace the example entry for a keyword-only run. Adding both a page and a keyword starts two independent searches.

#### 🎛️ Settings

Enter at least one item in `targets`. Existing API calls can still use `searchTerms`; the form hides this field. If you supply both fields, each entry starts a separate search. All searches share one total `maxAds` limit. Dates use Coordinated Universal Time (UTC).

| Setting              | Allowed values and default                                                                     | Effect                                                                               |
| -------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `targets`            | Up to 40 IDs, supported URLs, or keywords; omitted means none                                  | One search per entry; use page URLs for one advertiser                               |
| `searchTerms`        | Up to 20 non-empty strings; omitted means none                                                 | Compatibility field for existing API keyword searches                                |
| `countries`          | Up to 5 library country codes; omitted or `[]` follows the URL, otherwise `US`                 | Select library search scope; use `["ALL"]` alone for all countries                   |
| `activeStatus`       | `active`, `inactive`, `all`; default `active`                                                  | Select source active status                                                          |
| `adType`             | `all`, `political_and_issue_ads`, `housing_ads`, `employment_ads`, `credit_ads`; default `all` | Select source ad category                                                            |
| `mediaType`          | `all`, `image`, `video`; default `all`                                                         | Select source media filter                                                           |
| `publisherPlatforms` | Up to 4: `facebook`, `instagram`, `messenger`, `audience_network`; default `[]`                | Empty follows the URL, otherwise applies no platform filter                          |
| `contentLanguages`   | Up to 10 two-letter lower-case codes, such as `en`; default `[]`                               | Empty follows the URL, otherwise applies no language filter                          |
| `searchMode`         | `broad`, `exactPhrase`; default `broad`                                                        | Choose keyword matching                                                              |
| `sortBy`             | `impressions`, `recent`, `activeTime`; default `impressions`                                   | Source order within each search; recent uses month-based relevance groups            |
| `mode`               | `ads` or `count`; default `ads`                                                                | Collect rows, or read per-search source totals in the summary                        |
| `startDatePreset`    | `any`, `last7`, `last30`, or `custom`; optional                                                | Filter when ads started; relative presets include today in UTC                       |
| `startedOnOrAfter`   | Optional `YYYY-MM-DD`                                                                          | Include source start dates on or after this UTC date                                 |
| `startedOnOrBefore`  | Optional `YYYY-MM-DD`                                                                          | Include source start dates on or before this UTC date                                |
| `activeOnOrAfter`    | Optional `YYYY-MM-DD`                                                                          | Lower bound of an inclusive UTC delivery window                                      |
| `activeOnOrBefore`   | Optional `YYYY-MM-DD`                                                                          | Upper bound of an inclusive UTC delivery window                                      |
| `maxAds`             | Non-negative whole number; default `100`; `0` means no ad-count cap                            | One saved unique-ad cap across the run; no fixed maximum; blank and null are invalid |
| `details`            | `none`, `advertiser`, `transparency`, `all`; default `none`                                    | Request extra public fields; more requests can take longer                           |

The form's **Use URL setting** choice has the API value `auto`. It is available for `activeStatus`, `adType`, `mediaType`, `searchMode`, and `sortBy`. It follows a pasted URL and otherwise uses the default listed above. Empty country, platform, and language lists also follow the URL. Conflicting explicit form and URL filters produce an unsupported-input outcome. Unsupported URL selection fields are not silently removed.

**Ad start dates** provides Any time, Last 7 days, Last 30 days, and Custom start dates. Relative presets resolve at the start of each run, including scheduled runs. Last 7 days means today and the six previous UTC calendar dates. The summary records the resolved bounds. Choose `custom` to enter at least one start-date bound. Do not combine another preset with custom bounds. Existing API calls that omit `startDatePreset` can still supply their date bounds directly.

Start-date filters test when an ad started. Activity-window filters keep source delivery intervals that overlap the requested UTC dates. A currently active ad has no known end for this check. An inactive ad that started before the lower bound needs a valid source end time. Missing dates needed to decide overlap produce an incomplete input outcome. You can combine the two date filter types. The Actor checks them before details and saving. Selective filters can require reading many candidates.

Sorting follows Meta's order within each search. Recent mode uses relevance groups by month, not a strict start-time sort. ActiveTime mode orders by Meta's reported active time and can help find long-running ads. Inputs run in input order; the Actor does not combine all inputs into one global sort. Card order is the order observed in that response and can vary across requests.

Use `maxAds: 0` to remove the ad-count cap. This does not remove the spending limit, run time limit, source access limits, or available inventory. Large runs still need sufficient time and budget.

#### 🔎 Search by keyword

```json
{
  "targets": ["Nike"],
  "countries": ["US"],
  "contentLanguages": ["en"],
  "searchMode": "exactPhrase",
  "maxAds": 25,
  "details": "none"
}
```

Keywords can find resellers and other advertisers that use the term. A brand-name search does not restrict results to that brand's own page. Use an advertiser target for that purpose.

#### 📊 Read counts without collecting ads

```json
{
  "targets": ["15087023444"],
  "countries": ["US", "DE"],
  "mode": "count"
}
```

Open **Run summary and input outcomes** for the preview, or **Run summary and counts (JSON)** for API data. Each attempt has `sourceReportedTotal` and `sourceReportedTotalPrecision`. Counts can be approximate or unavailable and can overlap across inputs. Do not add them to claim a unique-ad total. Count mode reads only the first source result per search. It creates no ad rows and no Actor event charges. `maxAds` is ignored. Date filters and additional details are not supported in count mode.

#### 📅 Find ads active during a window

```json
{
  "targets": ["15087023444"],
  "activeStatus": "all",
  "activeOnOrAfter": "2026-10-01",
  "activeOnOrBefore": "2026-10-25",
  "sortBy": "recent",
  "maxAds": 100
}
```

This can include an ad that started before October and remained active during the window. It is based on source dates and flags, not a complete historical delivery log.

#### 🧾 Request advertiser and EU details

```json
{
  "targets": ["15087023444"],
  "countries": ["DE"],
  "mediaType": "video",
  "publisherPlatforms": ["instagram"],
  "maxAds": 3,
  "details": "all"
}
```

`advertiser` requests public page and linked Instagram information. `transparency` requests public audience and payer/beneficiary disclosures. `all` requests both. Availability varies by ad, region, category, and time. Start with a small limit when requesting details.

### ⬆️ Output sample

This shortened, illustrative partnership record shows the structure. Media URLs use placeholders. The full dataset has 44 top-level fields.

```json
{
  "adArchiveId": "1829888761376507",
  "adUrl": "https://www.facebook.com/ads/library/?id=1829888761376507",
  "pageId": "16453004404",
  "pageName": "Sephora",
  "pageUrl": "https://www.facebook.com/sephora/",
  "creativePage": {
    "pageId": "492109351169937",
    "pageName": "Jazzy Le",
    "pageUrl": "https://www.facebook.com/100066338425888/",
    "pageProfileImageUrl": "https://media.example.com/page.jpg",
    "pageCategories": null,
    "pageIsDeleted": false,
    "pageLikeCount": null
  },
  "publisherPlatforms": ["facebook", "instagram"],
  "displayFormat": "VIDEO",
  "creatives": [
    {
      "creativeIndex": 0,
      "sourceKind": "snapshot",
      "bodyText": "<source ad copy>",
      "title": null,
      "linkDescription": null,
      "linkCaption": null,
      "linkUrl": null,
      "ctaText": null,
      "ctaType": null,
      "images": [],
      "videos": [
        {
          "hdUrl": "https://media.example.com/video-hd.mp4",
          "sdUrl": "https://media.example.com/video-sd.mp4",
          "previewImageUrl": "https://media.example.com/preview.jpg",
          "watermarkedHdUrl": null,
          "watermarkedSdUrl": null
        }
      ]
    }
  ],
  "creativeCount": 1,
  "creativeVariants": {
    "texts": [],
    "linkUrls": [],
    "images": [],
    "videos": []
  },
  "isActive": true,
  "status": "ok",
  "statusReason": null
}
```

#### 🖼️ Read creative records

For an ad with cards, `creatives` contains one ordered entry per source card. `creativeIndex` is a position within this observation. It is not a stable creative ID across runs. For an ad without cards, it contains one snapshot entry. Each entry keeps its own copy, link, images, and videos.

Images can include original, resized, and watermarked URLs. Videos can include source HD and SD URLs, previews, and watermarked versions. Unavailable versions remain `null`.

`creativeVariants` keeps extra source lists separately. Array positions do not prove that an extra text, link, and video belong together. `creativeCount` counts main cards or the snapshot, not these unpaired variants. Neither count measures total advertiser inventory.

#### 📌 Read matches and missing fields

This shortened example shows one ad found by two processed inputs. The single ad row is charged once. `inputIndex` starts at zero.

```json
{
  "adArchiveId": "1016451784037642",
  "matches": [
    { "inputIndex": 0, "input": "15087023444", "country": "US" },
    { "inputIndex": 1, "input": "Nike", "country": "US" }
  ],
  "advertiserDetails": null,
  "fieldStates": [
    {
      "field": "advertiserDetails",
      "state": "not_requested",
      "reason": "Public advertiser details are unavailable or were not requested."
    }
  ]
}
```

`matches` contains only processed inputs that actually returned the ad. It does not claim every possible match. `fieldStates` explains selected missing or invalid fields by field path. It is not a list of every nullable nested field. For example, `not_requested` means details were off; `not_exposed` means the public response did not supply the value. False, zero, and an explicit empty list are valid values.

`videoPreviewUrls` gives flat preview links for exports. Use `creatives[].videos[]` to keep each preview tied to its video. `containsDigitallyCreatedMedia`, `containsSensitiveContent`, and `isEuTransparencyEligible` preserve source flags. A digitally-created-media flag does not independently prove AI use. EU eligibility does not guarantee that reach details are available.

#### 🤝 Read partnership ads

Top-level page fields describe the advertiser. `creativePage` describes the source creative publisher. Those pages can differ. Missing advertiser profile images, categories, or page-like counts are not filled with the creator's values.

#### 🕒 Read observation times

`observedAt` is the time the Actor received the first retained ad response. It is not the ad's start time or a guarantee that all nested values were read at that time.

- `advertiserDetails.profileObservedAt` records the profile detail response time.
- `advertiserDetails.pageObservedAt` records the public page response time. That page response can be reused for several ads in the run.
- `advertiserDetails.aboutTextObservedAt` records the response used for the selected About text.
- `disclosures.euAudience.observedAt` records the EU detail response time.

These times stay `null` when the response time is unavailable. They are collection times, not source publication or update times.

#### 📌 Read the run summary

The **Run summary and input outcomes** output uses schema version `1.4.0`. It includes `runId`, `buildId`, `startedAt`, `finishedAt`, `mode`, and `observationPolicy: "first_retained"`. It describes saved unique ads, saved creative units, duplicate and filtered candidates, and an outcome for each input-country attempt.

`effectiveInput` records the normalized ad cap, detail choice, and four local date bounds. Relative start-date presets appear as their resolved UTC bounds. A cap of `0` means unlimited. In count mode, the cap is `null` because no ad rows are saved. Each attempt has a `selection` object. It records the resolved target and effective source filters, including filters from an Ad Library URL. The attempt's country and source URL stay beside that object. This records what the run requested. It cannot guarantee the same source response on a later run. If input validation fails, `mode` and `effectiveInput` can be `null`.

Each attempt includes input, country, source URL, matched-ad count, status, and stop reason. `max_ads` means the global ad cap was reached. Later distinct inputs are `not_attempted`; they do not trigger more collection or details. `cost_limit` means the remaining paid budget cannot cover another ad. `source_exhausted` means the source ended the result sequence. `count_only` means the attempt read its source total without saving ads. Time limits and access errors can leave valid saved rows with a `partial` run summary.

`successfulAdsSaved` counts saved ads that pass the billing rule. `uniqueAdsSaved` also includes free partial ads. `billableAds` reports recorded successful-ad charges after the ads were saved. It is zero when no successful ads were charged. It is `null` if a charge failed or the count is unavailable or inconsistent. It does not estimate charges from all saved rows.

An ad can match several inputs. Attempt-level counts overlap; do not add them to estimate unique ads. Source-reported search totals have an unknown scope or precision unless stated otherwise. They are not a verified global advertiser inventory.

### 🚦 Status, quality, and precision

- **Saved row `ok`:** ad identity and contracted creative structure passed the checks.
- **Saved row `partial`:** an identified ad has an unavailable or invalid field that affects its contract. Read `fieldStates` and `statusReason`.
- **Run `complete`:** accepted attempts finished under their stated stop conditions. A cap-limited run can be complete.
- **Run `partial`:** useful ads were saved, but an attempt or row could not fully finish.
- **Run `failed`:** collection failed. Rows already saved remain available.

Input outcomes distinguish unsupported input, no public data, blocked or challenged access, login-required responses, rate limits, and parsing failures. Failure outcomes are kept in the summary, not written as fake ad rows.

Spend, reach, and impression disclosures remain source-reported text or ranges. A range does not become an exact value. The Actor does not infer sales, campaign budget, conversions, or profitability. EU reach, target locations, and payer/beneficiary fields are separate source facts. Missing demographic cells are not zero.

#### ➕ Count data at the correct level

| Value                              | Safe use                                                                                                                                                           |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Saved ads                          | Count unique (`runId`, `adArchiveId`) pairs for one run. Across runs, distinguish observations from unique ad IDs.                                                 |
| `creativeCount`                    | Count main card or snapshot occurrences within the saved observations. This is not a count of globally unique assets.                                              |
| Page likes and Instagram followers | Treat them as page snapshots. The same value can repeat on many ad rows. Do not sum those rows.                                                                    |
| EU reach                           | Keep the reported total and each country, age, and gender value. Do not assume cells sum to the total. Do not sum reach across ads or runs to count unique people. |
| Spend and impression ranges        | Keep bounds, currency, and source text. Do not turn ranges into exact totals. The source may not expose the measurement period.                                    |
| `collationCount`                   | Treat it as a source group count. The group can repeat across ad rows.                                                                                             |
| Media URLs                         | Count available URL versions only. Several URLs can refer to the same asset.                                                                                       |

For example, one ad with two matches, two cards, and three reach cells becomes 12 rows if you expand all three lists together. It is still one ad. Count ads before expanding lists. Summarize each list on its own before joining results. Never sum a repeated page or reach value from the expanded rows.

### ⚠️ Limits and interpretation

- `maxAds` is a saved-row cap. Source availability, filters, source limits, the run time limit, or the spending limit can produce fewer rows.
- The Actor saves each ad once per run. `matches` lists only processed inputs that returned that ad. The global ad or spending limit can stop the run before later inputs are searched. For several inputs, rows can appear after the searches finish.
- Library country is search scope, not proof of an ad's target or reach countries. Read the separate source fields for those facts.
- A source end time can be present while `isActive` is true. It does not override the source active flag.
- Page likes and Instagram followers are page observations. Do not add them across ad rows.
- Media URLs can expire. Collect them again when you need a current URL. The Actor returns links; it does not host permanent media copies.
- Full creative extraction means the contracted cards and variants returned in the captured response, not every possible product variation.
- Historical data depends on what Meta exposes. Access and field availability can change. A requested ad limit does not guarantee that many results.

### ❓ FAQ

#### How do I export Facebook Ad Library data to CSV or Excel?

1. Enter an advertiser page, keyword, or Ad Library URL in **Pages, Ad Library URLs, or keywords**.
2. Set an ad limit and click **Start**.
3. Open the run’s dataset and select CSV or Excel from the export options.

Use JSON when you need each carousel card’s text, links, images, and videos together. Flat exports are useful for ad lists and spreadsheet research.

#### Can I track competitor ads on a schedule?

Yes. Save your advertiser inputs as an Apify Task and add a schedule. Compare saved observations by `adArchiveId` across runs. You must build the comparison or notification step in your workflow. The Actor does not send new-ad alerts or compute change reports. An ad missing from an incomplete or capped run does not prove it stopped.

#### Can I use this with n8n, Make, or Google Sheets?

Yes. Run the Actor through the Apify API or a supported Apify integration. Send the dataset fields you need to your next workflow step or spreadsheet. Use `adArchiveId` to match ads across runs. Keep nested creative records in JSON when your workflow needs individual cards or media versions.

#### Do I need a Facebook account or Meta API plan?

No. The Actor uses public Meta Ad Library data. You need an Apify account to run it.

#### Why is the result count lower than my limit?

The limit is an upper bound. Filters may match fewer ads, the source may end or limit results, or the run may stop early. Read input outcomes and stop reasons in the summary.

#### Does a carousel or dynamic product ad cost more?

No. It is one saved ad row. Nested cards, media, and variants do not create more ad charges. Only a successful saved ad is charged.

#### Why are spend or reach fields null?

Meta does not expose those disclosures for every ad. Missing is not zero. Category or country selection does not guarantee a disclosure.

#### Can I use several inputs without duplicate ads?

Yes. The Actor removes duplicates by archive ID within a run and records matching inputs in `matches`. A later run is a new observation.

### 🔒 Source boundary and support

This is an independent tool for public Meta Ad Library data. It is not affiliated with Meta. Respect creative owners' rights when using or sharing their content.

Use the Actor’s Issues tab in Apify Store to report a problem. Include the run ID, public target or search input, expected field or count, and an example result. State whether it affects one ad or the whole run. Do not send account credentials.

# Changelog

This Actor's version history is a separate document: https://apify.com/chronometrica/facebook-ads-scraper/changelog.md

# Actor input Schema

## `targets` (type: `array`):

Add up to 40 entries. Use a Facebook page URL or numeric page ID to find that advertiser’s ads. Enter keywords to search across advertisers. Paste an Ad Library URL to keep its supported filters. For one exact ad, use its Library URL with id=. Each entry starts a separate search. The total ad limit covers all entries and countries.

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

Set one total saved-ad limit for all entries and countries. The API default is 100; the form starts with 10. Use 0 for no ad-count cap. Spending, time, and source limits still apply. An ad with several creative cards counts as one ad. Count mode ignores this limit.

## `countries` (type: `array`):

Leave blank to use each Ad Library URL’s country. Page IDs, page URLs, and keywords use United States when no country is set. To choose your own search countries, select 1–5. Select All countries on its own. Your choice must agree with a country in a pasted URL. The country sets the Library search scope. It does not prove ad targeting or reach.

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

Use URL setting follows the status in a pasted Ad Library URL. Other inputs use active ads. Your explicit choice must agree with the URL. Meta may not expose all inactive ads.

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

Choose an ad category, or follow the Ad Library URL. Without a URL category, the default is all categories. A category does not guarantee spend or reach data.

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

Choose image ads, video ads, or all media. Use URL setting follows a pasted Ad Library URL; other inputs use all media. A dynamic ad can contain several media types.

## `publisherPlatforms` (type: `array`):

Select publisher platforms, or leave empty to keep URL filters and otherwise search all platforms. An explicit selection must agree with a pasted URL.

## `contentLanguages` (type: `array`):

Choose a suggested language or enter a two-letter code in lower case, such as en. Leave blank to follow the Ad Library URL, or search all languages when the URL has no language filter. Your explicit choice must agree with the URL.

## `searchMode` (type: `string`):

Follow a pasted keyword search mode, or choose broad match or an exact phrase. The default for plain keywords is broad. This does not filter an advertiser by an added keyword; each input is a separate search.

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

Follow the URL sort, or choose an order within each search. The default is most impressions. Recent uses Meta’s relevance groups by month, not a strict start-date sort. Inputs stay in input order; there is no combined sort. Exact-ad URLs ignore this choice.

## `startDatePreset` (type: `string`):

Any time applies no start-date filter. Last 7 days and Last 30 days include today in UTC. Choose Custom start dates to enter at least one date below. Do not combine another preset with custom dates.

## `startedOnOrAfter` (type: `string`):

Optional inclusive source ad start date in YYYY-MM-DD, measured in UTC. Omit for no lower bound. This tests when the ad started, not whether it was active during a date window. Date filtering can require reading more candidates.

## `startedOnOrBefore` (type: `string`):

Optional inclusive source ad start date in YYYY-MM-DD, measured in UTC. Omit for no upper bound. Must not be earlier than startedOnOrAfter. Ads without a valid source start time cannot satisfy a date filter.

## `activeOnOrAfter` (type: `string`):

Keep ads whose source delivery interval overlaps this date or a later date. Inclusive YYYY-MM-DD in UTC. A currently active ad has no known delivery end. Missing dates needed to decide overlap produce an incomplete input outcome. Omit for no lower bound. Collection checks this filter before details and saving; selective windows can take longer. Not supported in count mode.

## `activeOnOrBefore` (type: `string`):

Keep ads whose source delivery interval overlaps this date or an earlier date. Inclusive YYYY-MM-DD in UTC. Must not be earlier than activeOnOrAfter. May be combined with start-date filters. Omit for no upper bound. Collection checks this filter before details and saving; selective windows can take longer. Not supported in count mode.

## `mode` (type: `string`):

Choose Ad records to save unique ads. Choose Counts only to read Meta’s reported total from the first result page of each search. Totals appear in the run summary. Counts only saves no ad rows and has no Actor event charge. Totals can be approximate, overlap across searches, or be unavailable. Counts only does not support date filters or extra data.

## `details` (type: `string`):

Standard ad data includes ad copy, creative cards, links, and media URLs. Add public advertiser and Instagram details, audience and payer disclosures, or both. There is no extra ad fee, but collection can take longer. Meta may not expose every field; missing values stay null. Keep Standard ad data for Counts only.

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

Compatibility input for existing API calls: up to 20 keyword searches. New form users can put keywords in targets. If both fields are supplied, their entries are separate searches and share the total ad limit.

## Actor input object example

```json
{
  "targets": [
    "https://www.facebook.com/nike/"
  ],
  "maxAds": 10,
  "countries": [],
  "activeStatus": "auto",
  "adType": "auto",
  "mediaType": "auto",
  "publisherPlatforms": [],
  "contentLanguages": [],
  "searchMode": "auto",
  "sortBy": "auto",
  "startDatePreset": "any",
  "mode": "ads",
  "details": "none"
}
```

# Actor output Schema

## `ads` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `summaryJson` (type: `string`):

No description

# 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 = {
    "targets": [
        "https://www.facebook.com/nike/"
    ],
    "maxAds": 10,
    "countries": [],
    "activeStatus": "auto",
    "adType": "auto",
    "mediaType": "auto",
    "publisherPlatforms": [],
    "contentLanguages": [],
    "searchMode": "auto",
    "sortBy": "auto",
    "startDatePreset": "any"
};

// Run the Actor and wait for it to finish
const run = await client.actor("chronometrica/facebook-ads-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 = {
    "targets": ["https://www.facebook.com/nike/"],
    "maxAds": 10,
    "countries": [],
    "activeStatus": "auto",
    "adType": "auto",
    "mediaType": "auto",
    "publisherPlatforms": [],
    "contentLanguages": [],
    "searchMode": "auto",
    "sortBy": "auto",
    "startDatePreset": "any",
}

# Run the Actor and wait for it to finish
run = client.actor("chronometrica/facebook-ads-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 '{
  "targets": [
    "https://www.facebook.com/nike/"
  ],
  "maxAds": 10,
  "countries": [],
  "activeStatus": "auto",
  "adType": "auto",
  "mediaType": "auto",
  "publisherPlatforms": [],
  "contentLanguages": [],
  "searchMode": "auto",
  "sortBy": "auto",
  "startDatePreset": "any"
}' |
apify call chronometrica/facebook-ads-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,chronometrica/facebook-ads-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/chCavo8GZWtxsLW9Z/builds/5RdSOdvjaQm6jXhYi/openapi.json
