Meta Ad Library API: Programmatic Competitor Research Guide
The Meta Ad Library API lets you query ads programmatically. Free, requires a Meta developer app + access token. Best for EU commercial + regulated ads.
Meta Ad Library API: what it is and how to use it
TL;DR: The Meta Ad Library API (
graph.facebook.com/v20.0/ads_archive) lets you query Meta’s public ad database programmatically instead of through the web UI. It’s free, requires a Meta developer app and a standard access token, and returns structured JSON. Post-DSA (Digital Services Act, 2023) coverage is broad for EU commercial ads and regulated categories; non-EU commercial ads remain more restricted. Covers the API basics, a working Python example, practical use cases, and the real-world limitations you’ll hit.
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 IDad_creative_bodies— the ad’s text copy (as an array; multiple versions on carousel ads)ad_creative_link_titles— headlinesad_creative_link_descriptions— descriptionsad_delivery_start_time/ad_delivery_stop_time— ISO timestampspublisher_platforms—["facebook", "instagram", "messenger", "audience_network"]page_id/page_name— the advertiserad_snapshot_url— URL to the rendered ad- For regulated categories only:
impressions,spend,demographic_distribution,delivery_by_region
For commercial ads (non-regulated), you won’t get impressions, spend, or targeting data — the same limitation as the web UI. What you get is structured metadata for every ad plus the snapshot URL for the creative.
What you need to access it
Three things:
- A Meta developer account. Free, sign up at developers.facebook.com.
- An App. Create one in the developer dashboard. Use case: “Ad Library research” or “Other.”
- 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, no special permissions are needed for political/regulated ads — the API is open by design. For EU commercial ads (post-DSA), the same token works. For non-EU commercial ads (US, UK outside specific categories), API access is more limited — some returns may be incomplete compared to the web UI.
Hands-on: your first API request
The base endpoint:
https://graph.facebook.com/v20.0/ads_archive
Required query parameters:
access_token— your tokensearch_termsORsearch_page_ids— what to search forad_reached_countries— a comma-separated country list (e.g.["US", "DE", "FR"])ad_type—ALL,POLITICAL_AND_ISSUE_ADS,HOUSING_ADS,EMPLOYMENT_ADS, orCREDIT_ADS
A minimal curl example:
curl "https://graph.facebook.com/v20.0/ads_archive?\
access_token=YOUR_TOKEN&\
search_terms=protein+powder&\
ad_reached_countries=%5B%22US%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/v20.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/v20.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 = "US", 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 "US"
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:
python mal_api.py "protein powder" US
python mal_api.py "Apple" DE
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": '["US"]',
"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.
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).
- No targeting data — you see what platforms an ad runs on but not audience details.
- 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 deprecates Graph API versions annually. Plan to bump the version number in your scripts ~every 12 months.
- Coverage varies by region. EU commercial ads are well-covered post-DSA; US commercial ads via API can return incomplete results compared to the web UI.
Key takeaways
- Endpoint:
graph.facebook.com/v20.0/ads_archive. - Free access, requires Meta developer app + access token.
- Structured JSON responses with pagination — easy to pipeline into workflows.
- Best coverage: political/regulated + EU commercial (post-DSA).
- Weaker coverage: non-EU commercial ads.
- Practical pattern: weekly pull per competitor, diff against history, trigger analysis on new ads.
Related
- seo/meta-ad-library-mastery — the pillar
- how-to-access-meta-ad-library — web UI alternative
- how-to-search-meta-ad-library — search strategy in the UI
- competitor-ad-monitoring-workflow — putting the API in a recurring workflow
- glossary/ai-creative-reverse-engineering — pairing API data with analysis
Sources
- Meta Ad Library API documentation — official reference.
- Meta Graph API Explorer — interactive query tool.
- Meta Transparency Center — DSA Ad Repository — EU coverage details.