Skip to content

soxoj/AdsLibrary

 
 

Repository files navigation

Social Media Ad Library Sources

A collection of data sources for the public ad libraries and ad transparency centers of major social platforms, behind one normalized interface. Query any supported platform with the same AdQuery and get back the same Ad shape — advertiser, creative, metrics, and mined contacts. Most sources require no authorization.

Supported sources

Platform Search by Contacts Metrics Auth
Meta / Facebook — scraper (MetaScraperSource) keyword · page · advertiser name ✅ email / phone / site none
Meta / Facebook — official Graph API (MetaGraphSource) keyword · page ✅ EU / political token
TikTok (TikTokSource) keyword · advertiser reach research client
Google Ads Transparency (GoogleAdsTransparencySource) advertiser none
LinkedIn Ad Library (LinkedInAdLibrarySource) keyword none
Snapchat Political Ads (SnapchatPoliticalAdsSource) keyword (metadata) ✅ address ✅ spend / impr none
X / Twitter Ads Repository (XAdsRepositorySource) advertiser (@handle) ✅ impr / reach none

Each source is documented in its own section below; the shared AdSource / AdQuery / Ad layer that makes them interchangeable is described under Multi-platform abstraction layer.

Use as an MCP server (plug into Claude)

The whole collection is exposed over the Model Context Protocol, so you can attach it to Claude (Code, Desktop, or any MCP client) and let the model call the sources as tools — no glue code.

pip install -e ".[mcp]"           # installs the `mcp` runtime
python -m AdsLibrary.mcp_server    # serves over stdio (or: ads-library-mcp)

Register it — Claude Code auto-loads a project-scoped .mcp.json; copy the template and it's picked up next time you open the repo (for Claude Desktop, paste the same block into its config):

cp .mcp.json.example .mcp.json
{
  "mcpServers": {
    "ads-library": {
      "command": "python",
      "args": ["-m", "AdsLibrary.mcp_server"],
      "env": { "META_ACCESS_TOKEN": "", "TIKTOK_CLIENT_KEY": "", "TIKTOK_CLIENT_SECRET": "" }
    }
  }
}

Tools exposed:

Tool What it does
list_sources every source, its platform, auth needs, and live capabilities
search_ads keyword / page / advertiser search on any source → normalized ads
search_advertisers resolve advertisers / pages by name
facebook_page_contacts no-auth: page name → its ads → mined emails / phones / websites

No-auth sources (Meta scraper, Google, LinkedIn, Snapchat, X) work immediately; meta_graph and tiktok read their credentials from the env vars above.

Origin — Facebook Ads contact scraper

The project began as a no-auth script that mines contact info (emails, phone numbers, websites) from the ads a Facebook Page runs. That still works and is the default MetaScraperSource today: resolve a page to its internal Ad Library id by name, pull its ads, and collect the unique contacts.

from AdsLibrary.main import FacebookAdsLibrary

fb = FacebookAdsLibrary()
page_id = fb.get_company_id("noticaribe peninsular")   # name -> internal page id
ads = fb.get_ads(page_id)["Ads"]

emails, phones, websites = set(), set(), set()
for a in ads:
    if a["Email"]:   emails.add(a["Email"])
    if a["Phone"]:   phones.add(a["Phone"])
    if a["Website"]: websites.add(a["Website"])
Internal Facebook page id is 116812033038668
Emails:   marca@noticaribepeninsular.com.mx, noticaribe2018@gmail.com, redaccion@noticaribepeninsular.com.mx
Phones:   +525583686255, +529981517796, +529984287875
Websites: https://noticaribepeninsular.com.mx/

The page comes from the study The Sad Fate of Small Facebook Audiences: Mexico. The live ad set grows over time, so the exact contacts returned may differ from run to run.


Official Meta Ad Library Graph API client

MetaAdLibraryGraphAPI wraps Meta's official Graph API endpoint (/{version}/ads_archive). Unlike the scraper above (which searches by page via internal endpoints), this client supports true keyword search and the full documented parameter / field surface.

Requires a Meta developer app + access token. Pass it explicitly or set the META_ACCESS_TOKEN environment variable.

How to get an access token

The official Ad Library API is gated behind identity confirmation:

  1. Confirm your identity at facebook.com/ID — upload a government ID and confirm your country. Takes ~1–3 business days. (This is the same flow used to run social-issue/political ads.)

  2. Create an app at Meta for DevelopersMy AppsCreate App, and accept the Platform Policy.

  3. In the app dashboard, Add Product → "Ad Library API" (exposes the ads_archive endpoint) and request the ads_read permission.

  4. Generate a token in the Graph API Explorer: pick your app, generate a User Token. For longer life, exchange it for a long-lived token (~60 days). Verify it with a GET /me call.

  5. Put it in the environment (or a git-ignored .env at the repo root):

    export META_ACCESS_TOKEN="EAAB..."
    # or:  echo 'META_ACCESS_TOKEN=EAAB...' >> .env

Notes: the current stable Graph API version is v22.0+ (v17 and earlier are rejected as of 2026); since Oct 2025 Meta no longer accepts new EU political ads, so the EU political corpus is frozen going forward.

Scope (Meta policy, not a code limit)

  • ad_type=ALL returns all commercial ads only for ads delivered to the EU/UK. Outside the EU, broad keyword search is effectively limited to political & issue ads.
  • spend, impressions, currency, demographic_distribution are populated only for political ads.
  • eu_total_reach, beneficiary_payers, targeting fields are populated only for ads delivered to the EU.

Usage

from AdsLibrary import MetaAdLibraryGraphAPI

api = MetaAdLibraryGraphAPI(access_token="EAAB...")  # or META_ACCESS_TOKEN env

# Free-text keyword search
ads = api.search_by_keyword("running shoes",
                            ad_reached_countries=["DE"],  # EU -> all ads
                            max_results=50)

# Political & issue ads worldwide (unlocks spend / impressions / demographics)
ads = api.search_political_ads("climate", ad_reached_countries=["US"], max_results=100)

# All commercial ads delivered to the EU (full EU field set)
ads = api.search_eu_ads("crypto", countries=["FR", "DE"])

# By page(s) — up to 10 IDs
ads = api.search_by_page(["116812033038668"])

# Stream large pulls without buffering everything
for ad in api.iter_ads(search_terms="vpn", ad_reached_countries=["IT"], max_results=1000):
    ...

# Helpers
api.simplify_ad(ads[0])        # flat, platform-agnostic record
api.collect_contacts(ads)      # {emails, phones, websites} across all ads

Method pack

Method Purpose
search_ads(**params) Full parameter surface, auto-pagination → list
iter_ads(**params) Same, but a streaming generator
search_by_keyword(kw, exact=False) Free-text search shortcut
search_by_page(page_ids) Ads by page id(s), max 10
search_political_ads(kw) ad_type=POLITICAL_AND_ISSUE_ADS + political fields
search_eu_ads(kw, countries=...) All EU commercial ads + EU fields
extract_contacts(ad) / collect_contacts(ads) Email/phone/website mining
simplify_ad(ad) Flatten to a compact normalized record

Field groups are exposed as class constants: CORE_FIELDS, EU_FIELDS, POLITICAL_FIELDS, ALL_FIELDS. Enum values are validated client-side (AD_TYPES, MEDIA_TYPES, PUBLISHER_PLATFORMS, ...) so bad params fail fast.


Multi-platform abstraction layer (AdSource)

To add other ad libraries (Google, TikTok, LinkedIn, Snapchat, X) as interchangeable backends, the project exposes a normalized layer in AdsLibrary.core and adapters in AdsLibrary.sources.

  • Models (core.models): Ad, Advertiser, Creative, Contacts, Metrics, plus the request object AdQuery. Every model keeps a raw field with the original platform payload.
  • Interface (core.base): AdSource with search_ads(query) and optional search_advertisers(name). Each source declares a SourceCapabilities (keyword vs page vs advertiser search, metrics, contacts, auth). Asking for an unsupported capability raises UnsupportedCapability instead of failing obscurely.
  • Adapters (core.sources): MetaGraphSource (official API) and MetaScraperSource (internal-endpoint scraper) both implement AdSource.
from AdsLibrary import AdQuery, MetaGraphSource, MetaScraperSource

# Official API: real keyword search (needs a token)
src = MetaGraphSource(access_token="EAAB...")
ads = src.search(keyword="running shoes", countries=["DE"], limit=50)

# Scraper: no keyword search, but resolves an advertiser by name then pulls ads
src = MetaScraperSource()
ads = src.find_ads_by_advertiser_name("Marine Le Pen")

for ad in ads:
    print(ad.platform, ad.advertiser.name, ad.title, ad.contacts.emails)

Capability matrix

Source keyword by page by name metrics contacts auth
MetaGraphSource ✅ (EU-wide; political worldwide) ✅ (political/EU) ✅ token
MetaScraperSource ✅ (first page, ~30 ads)
TikTokSource ✅ (business id) reach only ❌ (no ad text) ✅ research client
GoogleAdsTransparencySource ❌ (advertiser only) ✅ (advertiser id) ❌ (no ad text)
LinkedInAdLibrarySource
SnapchatPoliticalAdsSource ✅ (metadata) ✅ spend+impr ✅ address
XAdsRepositorySource ❌ (advertiser only) ✅ (@handle) ✅ impr+reach

The scraper parses Facebook's server-rendered Relay payload (the old async/search_* endpoints were retired). It returns the first page only (~30 ads); deeper pagination would require the rotating GraphQL doc_id.

Tests

pip install -r requirements-dev.txt

pytest                  # everything, incl. live e2e (scraper hits real Facebook)
pytest -m "not live"    # fast: deterministic unit tests only, no network

Two layers:

  • Unit tests drive the Graph client with a fake HTTP session and the scraper adapter with a fake client — fully offline, asserting that a search maps onto the normalized Ad model (ids, advertiser, creative, mined contacts) exactly.
  • Live e2e (tests/test_live.py, marked live) run by default and verify the upstream actually works. The scraper test needs no auth. The Graph API test runs when META_ACCESS_TOKEN is set (env or .env); without a token it is reported xfail, not silently skipped.

TikTok Commercial Content API

TikTokSource wraps TikTok's research Ad Library (/v2/research/adlib/...). It supports keyword + advertiser search and reach metrics, but carries no ad text (so no contact mining), and needs an approved research client.

from AdsLibrary import TikTokSource

src = TikTokSource(client_key="...", client_secret="...")  # or env vars
ads = src.search(keyword="mobile games", countries=["FR"], limit=20)
advertisers = src.search_advertisers("Acme")

How to get access:

  1. Create a TikTok for Developers account at developers.tiktok.com with your official email.

  2. Apply for the Commercial Content API product and a research client (application form + approval — this is the bottleneck, not instant).

  3. On approval you get a client key and client secret (scope research.adlib.basic). The client exchanges them for a bearer token automatically via the client-credentials flow.

  4. Put them in the environment (or a git-ignored .env):

    export TIKTOK_CLIENT_KEY="..."
    export TIKTOK_CLIENT_SECRET="..."

Notes: coverage is EU-first; the API is for research/compliance, not commercial use; one country_code per query. ad_published_date_range is required and its max must be a date strictly before today (the client defaults to the last 30 days ending yesterday when you don't pass dates).

Verification status (live, with real research credentials):

  • search_advertisers (advertiser query) — ✅ verified working.
  • search / ad query — ⚠️ TikTok returns HTTP 500 internal_error server-side (as of 2026-06-13) on every well-formed request; advertiser query works with the same token, so this is upstream, not a client bug. Retry later / contact commercial-research-questions@tiktok.com with the log_ids.

Google Ads Transparency Center

GoogleAdsTransparencySource scrapes Google's Transparency Center internal RPC endpoints (/anji/_/rpc/...). No auth. Google has no ad-text keyword search — you search by advertiser (name → advertiser id → creatives). Ads carry no text, so no contacts.

from AdsLibrary import GoogleAdsTransparencySource

src = GoogleAdsTransparencySource()
advertisers = src.search_advertisers("Nike")        # -> [Advertiser(id="AR..."), ...]
ads = src.find_ads_by_advertiser_name("Nike")       # name -> advertiser -> creatives
# each Ad: id="CR...", advertiser, first/last shown dates, creative preview URL

Returns the first page of creatives per advertiser. Region filtering uses Google's numeric region enum (pass regions=[...] to the low-level client).

LinkedIn Ad Library

LinkedInAdLibrarySource parses LinkedIn's public, server-rendered Ad Library. No auth, and it supports free-text keyword search.

from AdsLibrary import LinkedInAdLibrarySource

src = LinkedInAdLibrarySource()
ads = src.search(keyword="cybersecurity", limit=10)
# each Ad: id, advertiser name, body (commentary), detail URL, format (in .raw)

# enrich=True fetches each ad's detail page for the full text + paying entity
src = LinkedInAdLibrarySource(enrich=True)

Returns the first page of results. Search cards give advertiser + a truncated commentary preview; pass enrich=True (one extra request per ad) for the full text and the "Paid for by" entity. No paid metrics; no contact mining.

Snapchat Political Ads Library

SnapchatPoliticalAdsSource reads Snapchat's public political-ads CSV dumps. No auth. Political/issue ads only (Snap has no commercial library).

from AdsLibrary import SnapchatPoliticalAdsSource

src = SnapchatPoliticalAdsSource()              # defaults to the current year
ads = src.search(keyword="cornerstone", limit=10)   # matches org/committee/candidate
ads = src.search(limit=50)                       # empty query browses the whole dump

# specific years:
src = SnapchatPoliticalAdsSource(years=[2024, 2025, 2026])

The dump is downloaded once and cached per year. Each Ad has spend + impressions (metrics), the advertiser billing address (contacts.addresses), country and dates. There is no creative text — keyword search matches the ad's metadata (organization / committee / candidate / jurisdiction), and the creative is an asset URL.

X (Twitter) Ads Repository

XAdsRepositorySource drives X's DSA "Ads repository". No login required — the whole flow runs on a guest session. Search is by advertiser @handle + country + date range (no keyword); EU-only, and the repository is sparse.

from AdsLibrary import AdQuery, XAdsRepositorySource

src = XAdsRepositorySource()
advertisers = src.search_advertisers("nike")          # @handle -> advertiser id
ads = list(src.search_ads(AdQuery(advertiser_ids=["nike"], countries=["DE"],
                                  date_min="2026-01-01", date_max="2026-06-30")))
# each Ad: advertiser, creative body, Impressions + Reach (metrics), dates

How it works (reverse-engineered): warm up ads.x.com/ads-repository for guest cookies → user-search resolves the handle → CreateExportReportMutation (cookies + x-csrf-token, no bearer) starts a CSV export → poll GetExportReportStatusQuery (with the public web bearer) until Finished → download the public CSV. The export takes minutes to build. CSV columns: Advertiser Name, Funding Entity, Creative, Start date, End Date, Targeted Segments, Impressions, Reach, plus removal/enforcement fields (no per-ad id).

Adding a new platform

Create AdsLibrary/sources/<platform>.py with a subclass of AdSource that (1) returns its capabilities, (2) implements search_ads(query) mapping the platform payload into Ad, and (3) optionally implements search_advertisers. Export it from sources/__init__.py. Done — it now works with the same AdQuery and Ad shapes as every other source.

Contributors

Languages

  • Python 100.0%