Skip to content

Meta Ad Library API: Programmatic Competitor Research Guide

Meta Ad Library API: what it is and how to use it

TL;DR: The Meta Ad Library API (graph.facebook.com/v26.0/ads_archive as of Oct 2026) lets you query Meta’s public ad database programmatically instead of through the web UI. It’s free, requires a Meta developer app, an access token and a one-time identity/location confirmation, and returns structured JSON. Coverage is set by regulation: every ad delivered to the EU (Digital Services Act) plus social-issue, electoral and political ads worldwide. Commercial ads that never reached the EU are not returned at all, so a US-only competitor is invisible to the API. Covers the API basics, a working Python example, practical use cases, and the real-world limitations you’ll hit.

If you’d rather hand off the creative work that follows the research, Primores makes performance ad creatives: Performance creatives.

The Ad Library web UI is fine for ad-hoc browsing. For anything repeatable — monitoring 50 competitors weekly, pulling historical ad inventories, feeding an AI creative reverse-engineering workflow with structured data — you’ll hit the web UI’s limits fast. The API is the answer.

What the API actually returns

Each ad in the API response comes as a structured record with fields like:

  • id — the unique Ad Library ID
  • ad_creative_bodies — the ad’s text copy (as an array; multiple versions on carousel ads)
  • ad_creative_link_titles — headlines
  • ad_creative_link_descriptions — descriptions
  • ad_delivery_start_time / ad_delivery_stop_time — ISO timestamps
  • publisher_platforms — ["facebook", "instagram", "messenger", "audience_network"]
  • page_id / page_name — the advertiser
  • ad_snapshot_url — URL to the rendered ad
  • For political/issue ads only: impressions, spend, demographic_distribution, delivery_by_region
  • For ads delivered to the EU only (DSA fields): eu_total_reach, age_country_gender_reach_breakdown, target_ages, target_gender, target_locations, beneficiary_payers

For commercial ads you won’t get impressions or spend. For EU-delivered commercial ads you do get EU reach and the advertiser’s age, gender and location targeting, but not interest or custom-audience targeting. What you always get is structured metadata plus the snapshot URL for the creative.

What you need to access it

Four things:

  1. Identity and location confirmation. Before your first call, complete Meta’s ID confirmation (the same process advertisers use to run political ads: government ID plus country of residence, at facebook.com/ID). Approval can take a few days.
  2. A Meta developer account. Free, sign up at developers.facebook.com.
  3. An App. Create one in the developer dashboard. Use case: “Ad Library research” or “Other.”
  4. An access token. For personal research use, a long-lived user access token works. For production/team use, configure a system user token. Tokens are created in the App’s dashboard.

Scope-wise, the Ad Library API normally needs no App Review and no ads_read/ads_management permissions; the identity confirmation is the gate. The same token covers EU-delivered ads and political/issue ads. Meta’s reference is explicit about everything else: ads that did not reach any EU location are returned only if they are about social issues, elections or politics. For a US-only commercial advertiser the API returns nothing, and you are back to the web UI (which shows their active ads).

Two dated caveats on political ads: Meta stopped delivering political, electoral and social-issue ads in the EU on October 6, 2025 (in response to the EU’s TTPA regulation), so EU results in that category are an archive that ends on that date. Outside the EU, political/issue ads are still archived for 7 years.

Hands-on: your first API request

The base endpoint:

https://graph.facebook.com/v26.0/ads_archive

v26.0 is the current version as of Oct 2026; v25.0 is supported until July 2028. Core query parameters:

  • access_token — your token
  • search_terms OR search_page_ids — what to search for
  • ad_reached_countries — required; a country list (e.g. ["DE", "FR"]) or ALL. For commercial research, use EU countries: a US-only query with ad_type=ALL returns only political/issue ads
  • ad_type — ALL, POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, EMPLOYMENT_ADS, or FINANCIAL_PRODUCTS_AND_SERVICES_ADS (which replaced the old credit category)

A minimal curl example:

Terminal window
curl "https://graph.facebook.com/v26.0/ads_archive?\
access_token=YOUR_TOKEN&\
search_terms=protein+powder&\
ad_reached_countries=%5B%22DE%22%5D&\
ad_type=ALL&\
fields=id,ad_creative_bodies,page_name,ad_delivery_start_time,ad_snapshot_url"

The response is JSON:

{
"data": [
{
"id": "1234567890",
"ad_creative_bodies": ["Tired of bland protein shakes?..."],
"page_name": "Example Brand",
"ad_delivery_start_time": "2025-11-15T00:00:00+0000",
"ad_snapshot_url": "https://www.facebook.com/ads/archive/render_ad/?id=..."
}
],
"paging": {
"cursors": {"before": "...", "after": "..."},
"next": "https://graph.facebook.com/v26.0/ads_archive?..."
}
}

Pagination follows Meta’s standard cursor-based pattern — use the paging.next URL to get the next page.

A working Python example

A simple script that queries the API, handles pagination, and saves results to JSON:

#!/usr/bin/env python3
"""Pull Meta Ad Library ads for a keyword + country, save to JSON."""
import json
import sys
import time
import urllib.parse
import urllib.request
ACCESS_TOKEN = "YOUR_TOKEN_HERE"
ENDPOINT = "https://graph.facebook.com/v26.0/ads_archive"
FIELDS = [
"id",
"ad_creative_bodies",
"ad_creative_link_titles",
"ad_creative_link_descriptions",
"page_id",
"page_name",
"ad_delivery_start_time",
"ad_delivery_stop_time",
"publisher_platforms",
"ad_snapshot_url",
]
def query_ads(search_terms: str, country: str = "DE", limit: int = 100) -> list[dict]:
"""Query the Ad Library and return all matching ads (paginated)."""
params = {
"access_token": ACCESS_TOKEN,
"search_terms": search_terms,
"ad_reached_countries": f'["{country}"]',
"ad_type": "ALL",
"fields": ",".join(FIELDS),
"limit": limit,
}
url = f"{ENDPOINT}?{urllib.parse.urlencode(params)}"
all_ads = []
while url:
with urllib.request.urlopen(url, timeout=15) as resp:
data = json.loads(resp.read().decode("utf-8"))
ads = data.get("data", [])
all_ads.extend(ads)
print(f"fetched {len(ads)} ads (total: {len(all_ads)})", file=sys.stderr)
url = data.get("paging", {}).get("next")
time.sleep(0.3) # polite rate-limiting
return all_ads
if __name__ == "__main__":
search, country = sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "DE"
ads = query_ads(search, country)
out_file = f"ads_{search.replace(' ', '_')}_{country}.json"
with open(out_file, "w", encoding="utf-8") as f:
json.dump(ads, f, indent=2, ensure_ascii=False)
print(f"wrote {len(ads)} ads → {out_file}")

Usage:

Terminal window
python mal_api.py "protein powder" DE
python mal_api.py "Apple" FR

This is the pattern Primores’ internal monitoring uses. For production use, wrap it in a rate-limiting layer (Meta enforces request limits) and add retry logic for transient errors.

Searching by page instead of keyword

Advertiser-name search is more reliable than keyword search for competitor monitoring. Use search_page_ids instead of search_terms:

params = {
"access_token": ACCESS_TOKEN,
"search_page_ids": "12345678,90123456", # comma-separated page IDs
"ad_reached_countries": '["DE"]', # an EU country, or commercial ads won't return
"ad_type": "ALL",
"fields": ",".join(FIELDS),
}

You get page IDs from the web UI’s URL when viewing a specific brand’s ads (view_all_page_id=...), or programmatically via Meta’s Graph API Page search (requires additional permissions).

Practical use cases

Weekly competitor monitoring. Run the script once a week for your tracked competitors. Diff the result against last week’s output to surface new ads. Primores has this wired to a Claude Code skill that additionally runs reverse-engineering on any new ad.

Category-wide intelligence. Keyword search on category terms (“meal delivery,” “skincare”). Cluster results by advertiser, sort by ad count, identify emerging brands spending meaningful creative effort.

Longevity filtering. After pulling a batch, filter client-side to ads with ad_delivery_start_time older than 30 days and still active — those are high-probability winners worth deeper analysis. EU-delivered ads stay queryable for one year after they last ran, so you can also see which concepts were switched off.

Creative stock for LLM workflows. Feed the ad_creative_bodies and ad_snapshot_url into a multimodal LLM for structured creative analysis at scale.

Rate limits and best practices

Meta enforces Graph API rate limits per app and per user. Published limits are intentionally vague — Meta uses “a sliding window based on usage” rather than hard numbers. In practice:

  • A single process making sequential requests with a ~0.3s delay between them rarely hits limits.
  • Parallelizing too aggressively (10+ concurrent requests) will get you throttled or temporarily blocked.
  • Very large queries (pulling thousands of ads) should include exponential-backoff retry logic.

For monitoring 50 competitors weekly, total API volume is in the low thousands of requests per week — well within comfortable limits.

Limitations you’ll hit

  • No impressions or spend for commercial ads (API limitation mirrors the web UI).
  • Thin targeting data — EU-delivered ads expose age, gender and location targeting and EU reach; nothing exposes interest, lookalike or custom-audience targeting, and non-EU commercial ads expose nothing.
  • Image/video assets via snapshot URL, not direct download. You’ll still need to fetch the snapshot URL separately to get the creative asset itself, which is the same pattern as the web-UI download workflow.
  • API version deprecation. Meta ships new Graph API versions roughly twice a year and each is supported for about two years (v20.0, used in earlier versions of this page, expired Sep 24, 2026). Check the version in your scripts at least yearly.
  • Coverage is EU-or-political only. Commercial ads that never reached the EU are not returned at all. Monitor non-EU-only advertisers through the web UI or a third-party tool.

Key takeaways

  • Endpoint: graph.facebook.com/v26.0/ads_archive (as of Oct 2026; versions live about two years).
  • Free access, requires identity confirmation, a Meta developer app and an access token.
  • Structured JSON responses with pagination — easy to pipeline into workflows.
  • Coverage: all EU-delivered ads (kept one year after last delivery) plus political/issue ads worldwide (7 years; EU political archive ends Oct 6, 2025).
  • No coverage: commercial ads that never reached the EU.
  • Practical pattern: weekly pull per competitor, diff against history, trigger analysis on new ads.

Sources