# Facebook Political & Commercial Ad Transparency Extractor (`vexaloom-labs/facebook-political-and-commercial-ad-transparency`) Actor

Search the Facebook Ad Library by keyword, advertiser Page ID, Ad ID, or funding-entity name, and get ad text, media, run dates, platforms, and spend or impressions for political and issue ads.

- **URL**: https://apify.com/vexaloom-labs/facebook-political-and-commercial-ad-transparency.md
- **Developed by:** [vexaloom-labs](https://apify.com/vexaloom-labs) (community)
- **Categories:** Social media, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.49 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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 Political & Commercial Ad Transparency Extractor

Read Facebook's public Ad Library directly and export one clean JSON record per ad — creative copy, carousel cards, images and videos, placements, advertiser identity, run dates, and the transparency numbers Meta only publishes for political and EU-delivered ads (spend ranges, impressions, reach, payer/beneficiary, age/country/gender reach). Works with a plain guest session: no Facebook login, no cookies, no Meta developer app.

### What you get

- **Four ways to search** — keywords, advertiser Pages, exact Ad Library IDs, or the funding entity on a "Paid for by" disclaimer, all in one run if you want
- **Meta's own transparency payloads, re-emitted whole** — political spend and impression ranges; EU total reach plus the per-country age/gender reach breakdown; declared payer and beneficiary; audience age/gender/location distributions returned as Meta publishes them
- **Creative in full** — primary media URL (copied to a permanent URL before it can expire), every image and video URL, carousel cards with their own text/CTA/link, CTA type, caption, title and display format
- **Advertiser context** — Page URL, profile picture, likes, categories, verification, and the linked Instagram handle with follower count and About text
- **Scale signals** — `collation_count` (variants sharing the same creative), `days_running`, placements, and the special-ad categories
- **Honest gaps** — a field Meta does not disclose for an ad is simply left out; nothing is estimated or fabricated

### Data source

The actor talks to the same internal JSON endpoint the Ad Library web page uses (`facebook.com/api/graphql/`, guest session). The page's own query identifiers are re-learned from the live site when they rotate, so the actor keeps working across Meta releases. No credential of any kind is required. Requests run through a built-in connection ladder (direct first, an Apify datacenter pool, then residential as a last resort) chosen automatically per request — you never configure a proxy.

### Modes

| Mode | What it does |
|---|---|
| `search` | Keyword search across all advertisers (any-order or exact-phrase matching) |
| `byPage` | Every ad an advertiser Page is running or has run |
| `byAdId` | Fetch one or more specific ads by their Ad Library ID |
| `byByline` | Political/issue ads by their "Paid for by" funding entity |
| (empty) | Processes every target list you filled in, in a single run |

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | select | All provided targets | Which target list to use; empty processes all of them |
| `searchTerms` | array | – | Keywords, one search each |
| `pageIds` | array | – | Numeric Page IDs or URLs containing `view_all_page_id=` / `?id=` |
| `adIds` | array | – | Numeric Ad Library IDs or ads-library URLs containing `?id=` |
| `bylines` | array | – | "Paid for by" funding-entity names (exact string matching; always searched in the political & issue archive, where this filter exists) |
| `country` | select | `ALL` | Single country code; ignored when `countries` is set |
| `countries` | multi-select | – | Several country codes searched together (true union) |
| `adActiveStatus` | select | `active` | `all` / `active` / `inactive` |
| `adType` | select | `all` | `all` / `political_and_issue_ads` / `housing` / `employment` / `credit` |
| `mediaType` | select | `all` | `all` / `image` / `video` / `meme` / `none` |
| `publisherPlatforms` | multi-select | – | FACEBOOK / INSTAGRAM / MESSENGER / AUDIENCE_NETWORK / WHATSAPP / THREADS |
| `contentLanguages` | array | – | ISO 639-1 codes of the ad creative (e.g. `en`, `de`) |
| `searchType` | select | `keyword_unordered` | Any-order or exact-phrase keyword matching |
| `startDate` | date | – | Ads with impressions on/after this date (delivery window) |
| `endDate` | date | – | Ads with impressions on/before this date |
| `sortBy` | select | `relevance` | `relevance` or `total_impressions` |
| `resultsPerSearch` | integer | `50` | Cap per search target (1–100000) |
| `maxItems` | integer | `1000` | Global cap across everything (1–100000) |
| `includeEuDetails` | boolean | `true` | Fetch EU transparency, payer/beneficiary and advertiser profile per ad |
| `rehostMedia` | boolean | `true` | Copy the primary media into a permanent URL (originals kept in `media_url_original`) |
| `onlyTotal` | boolean | `false` | Return just the total result count per target, read from the Ad Library page in one lightweight request (no browser, no pagination) — respects every filter including the date window; Facebook caps displayed totals at 50,001 (50,000+) |

### Output fields

Every record is omit-empty (a field that does not apply is absent, never null).

| Field | Meaning |
|---|---|
| `ad_id`, `ad_snapshot_url` | Ad Library ID and its public snapshot URL |
| `page_id`, `page_name`, `page_url`, `page_profile_picture_url` | Advertiser identity |
| `page_like_count`, `page_categories`, `page_verification`, `page_is_deleted` | Advertiser page signals |
| `advertiser_ig_username`, `advertiser_ig_followers`, `advertiser_ig_verified`, `advertiser_about` | Linked Instagram profile and About text |
| `ad_text`, `title`, `caption`, `link_description`, `display_format` | Creative text (as Meta serves it) |
| `template_body`, `template_title`, `template_tokens`, `resolved_ad_text`, `resolved_title` | Dynamic-creative handling: raw `{{…}}` placeholders are kept in `template_body`/`template_title`, their token names in `template_tokens`, and a token-free `resolved_ad_text`/`resolved_title` is attached when the payload's carousel cards carry the served variant |
| `cta_text`, `cta_type`, `link_url` | Call to action and destination |
| `media_type`, `media_url`, `media_url_original`, `media_url_expires_at` | Primary creative media |
| `image_urls`, `video_urls`, `video_preview_urls` | All creative media URLs |
| `cards` | Carousel cards (title, body, caption, link, CTA, image, videos) |
| `start_date`, `end_date`, `days_running`, `status`, `is_active` | Delivery window and live status |
| `publisher_platform` | Placements (Facebook, Instagram, Messenger, …) |
| `collation_count`, `collation_id` | Creative-reuse group size and ID |
| `categories`, `gated_type`, `regional_regulation_data` | Special categories and regulation flags |
| `spend`, `spend_lower_bound`, `spend_upper_bound`, `currency` | Spend range (political & EU-regulated ads) |
| `impressions`, `impressions_index`, `impressions_lower_bound`, `impressions_upper_bound`, `reach_estimate` | Impression/reach signals (same gating) |
| `byline`, `disclaimer_label` | "Paid for by" funding entity |
| `eu_total_reach`, `age_country_gender_reach_breakdown` | EU DSA total reach and per-country age/gender breakdown |
| `target_age_audience`, `target_gender_audience`, `target_location_audience` | Declared EU/UK targeting |
| `audience_age_gender`, `audience_location`, `audience_single_country` | Audience distribution as published |
| `payer_beneficiary`, `violation_types`, `is_violating_eu_siep` | EU payer/beneficiary declarations and violations |
| `contains_digital_created_media`, `contains_sensitive_content`, `has_user_reported`, `is_aaa_eligible`, `total_active_time`, `hide_data_status`, `state_media_run_label` | Additional Meta flags |
| `search_term`, `scraped_at` | Which search produced the row, and when |

Diagnostic rows use `type: "facebook_ads_library_diagnostic"` with `searchTerm`, `outcome`, `message` and `scrapedAt`; `onlyTotal` runs push `type: "facebook_ads_library_total"` with `total_results`. Ad-ID lookups that neither the search endpoint nor the ad's own page can resolve produce a `detail_unavailable` diagnostic that reports Facebook's own reason (`NO_AD_FOUND`) — no record is ever fabricated.

### Example

```json
{
    "mode": "search",
    "searchTerms": ["coffee"],
    "country": "US",
    "adActiveStatus": "active",
    "includeEuDetails": true,
    "maxItems": 50
}
```

### FAQ

**Do I need a Facebook account or cookies?** No. The actor works with an anonymous guest session by default.

**Why is spend missing on some ads?** Meta publishes spend and impressions ranges only for political/issue ads and EU-regulated advertising. The actor returns those fields when the source has them and omits them otherwise — it never fills a gap with a guess.

**Why did my political search for an EU country return all ad types?** Meta's political/issue archive does not cover EU/EEA countries; EU political ads live in the general transparency stream, so the actor automatically searches all types there and says so in the run log.

**Will image and video links keep working?** Facebook's media links are signed and expire within days. With `rehostMedia` on, the primary media is copied to a permanent URL first and the original is preserved in `media_url_original`; the arrays keep Meta's URLs, each with its expiry in `media_url_expires_at`.

**How far back does the data go?** Whatever the public Ad Library serves for your query: seven years for political/issue ads, one year for ads delivered to the EU, and live archive coverage for commercial ads.

**Can I scrape several countries or targets at once?** Yes — `countries` yields a union, and every target list can run together in one job.

**What is `collation_count`?** The number of ad variants Meta groups under the same creative and text — a quick way to spot heavily scaled campaigns.

**Why does `ad_text` sometimes contain `{{product.brand}}`?** Dynamic-creative (DCO/DPA) ads are stored with template placeholders; Meta resolves them only when the ad is delivered. The actor keeps the served text untouched in `ad_text`, records the placeholder in `template_body`/`template_tokens`, and attaches `resolved_ad_text`/`resolved_title` when the payload's cards contain the resolved creative instead of inventing one.

**Is the total count exact?** It is the number the Ad Library page itself displays for your filters. Meta caps the display at 50,001, meaning "50,000 or more"; below that it is exact. Totals are not available for byline searches because the Ad Library UI has no byline filter.

# Actor input Schema

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

What to scrape. Leave empty to process every target list you filled in (searchTerms, pageIds, adIds, bylines) in one run.

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

Keywords to search for in the Ad Library, one search per term. Example: \["coffee", "nike"].

## `pageIds` (type: `array`):

Facebook Page IDs to fetch all ads from (numeric IDs, or URLs containing view_all_page_id= or profile.php?id=). Each page is searched separately.

## `adIds` (type: `array`):

Ad Library IDs to fetch directly (numeric IDs or Ads Library URLs containing ?id=). All other filters are ignored in this mode.

## `bylines` (type: `array`):

Funding-entity / committee names from the "Paid for by" disclaimer on political and issue ads. Matching is exact (case-insensitive fallback): use the name as it appears in the ad. Byline targets always search the political & issue archive, since the Ad Library ignores this filter elsewhere.

## `country` (type: `string`):

Country to search ads in, or ALL for every country. Ignored when the Countries multi-select below is set.

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

One or more country codes, searched together as a union (unlike the single Country field). Takes precedence over Country when set.

## `adActiveStatus` (type: `string`):

Filter by ad active status.

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

Filter by ad category. Note: the political-and-issue archive excludes EU/EEA countries, so EU political searches automatically use All Types (EU transparency stream).

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

Filter by the ad's creative media type.

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

Only return ads shown on these platforms. Leave empty for all platforms.

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

2-letter ISO language codes (e.g. en, de, es) to restrict ads by creative language.

## `searchType` (type: `string`):

How keyword search terms are matched (mode=search only).

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

Only include ads that had impressions on or after this date (YYYY-MM-DD). Combined with End Date it forms a delivery window.

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

Only include ads that had impressions on or before this date (YYYY-MM-DD).

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

Order results by relevance (Facebook's default) or by total impressions.

## `resultsPerSearch` (type: `integer`):

Maximum number of ads to extract per search term / page / ad / byline.

## `maxItems` (type: `integer`):

Stop the whole run after this many ads, across all search targets.

## `includeEuDetails` (type: `boolean`):

Fetch per-ad detail data: EU total reach, age/country/gender reach breakdown, targeting, payer/beneficiary, advertiser Instagram profile and page About. Adds one extra request per ad.

## `rehostMedia` (type: `boolean`):

Copy each ad's primary media into this run's key-value store and output the permanent URL (Facebook's own media URLs expire after a few days). The original signed URL is kept in media_url_original.

## `onlyTotal` (type: `boolean`):

Skip ad scraping and return only the total number of results per search target (one lightweight page request, no browser). Counts respect the keyword, country, status, ad type, media type and date window; Facebook caps displayed totals at 50,001 (50,000+). Not available for byline searches.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "coffee"
  ],
  "country": "ALL",
  "adActiveStatus": "active",
  "adType": "all",
  "mediaType": "all",
  "searchType": "keyword_unordered",
  "sortBy": "relevance",
  "resultsPerSearch": 3,
  "maxItems": 3,
  "includeEuDetails": true,
  "rehostMedia": true,
  "onlyTotal": false
}
```

# Actor output Schema

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

Dataset containing all scraped Facebook ads, one JSON object per ad

# 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 = {
    "mode": "search",
    "searchTerms": [
        "coffee"
    ],
    "adActiveStatus": "active",
    "adType": "all",
    "mediaType": "all",
    "searchType": "keyword_unordered",
    "sortBy": "relevance",
    "resultsPerSearch": 3,
    "maxItems": 3,
    "includeEuDetails": false,
    "rehostMedia": false,
    "onlyTotal": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("vexaloom-labs/facebook-political-and-commercial-ad-transparency").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 = {
    "mode": "search",
    "searchTerms": ["coffee"],
    "adActiveStatus": "active",
    "adType": "all",
    "mediaType": "all",
    "searchType": "keyword_unordered",
    "sortBy": "relevance",
    "resultsPerSearch": 3,
    "maxItems": 3,
    "includeEuDetails": False,
    "rehostMedia": False,
    "onlyTotal": False,
}

# Run the Actor and wait for it to finish
run = client.actor("vexaloom-labs/facebook-political-and-commercial-ad-transparency").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 '{
  "mode": "search",
  "searchTerms": [
    "coffee"
  ],
  "adActiveStatus": "active",
  "adType": "all",
  "mediaType": "all",
  "searchType": "keyword_unordered",
  "sortBy": "relevance",
  "resultsPerSearch": 3,
  "maxItems": 3,
  "includeEuDetails": false,
  "rehostMedia": false,
  "onlyTotal": false
}' |
apify call vexaloom-labs/facebook-political-and-commercial-ad-transparency --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vexaloom-labs/facebook-political-and-commercial-ad-transparency"
        }
    }
}
```

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/6Dh4fWQXIee4jhMOE/builds/WuTbehzZYnu3x7Fwe/openapi.json
