Facebook Ads Library API Without App Review: Search Competitors' Ads and Track New Ones
Meta's Ad Library API covers political and EU ads and needs identity confirmation. Here's how to search any advertiser's Facebook and Instagram ads by keyword or page with one API key, read the response, and get alerted when a competitor launches a new ad.
Yes, you can read the Facebook Ads Library (Meta Ad Library) through an API without Meta's app review: SocialAPIs' /facebook/ads/search returns the active ads for any keyword or any advertiser's page as JSON, with one API key and no Meta developer app. Meta's own Ad Library API is narrower than most people expect: it requires identity confirmation and a developer app, and it covers ads about social issues, elections or politics, plus ads shown in the EU. For ordinary commercial ads elsewhere, the official API has no answer. This guide shows the calls, the response, and a small script that tells you when a competitor launches a new ad.
Meta's Ad Library API vs. a data API
| Meta Ad Library API | SocialAPIs | |
|---|---|---|
| Setup | Identity confirmation, a Meta developer app, an access token | One API key, sign up in a minute |
| Ads covered | Social-issue, election and political ads; ads shown in the EU | What the public Ad Library website shows: active and inactive ads, any category, any country it supports |
| Search by keyword | Yes, within its coverage | Yes |
| Search by advertiser | Yes, within its coverage | Yes (ad_page_id) |
| Spend and impressions | For political / issue ads | Only where Meta publishes them (mostly political and EU ads); null otherwise |
| Format | Graph API | Plain JSON over HTTPS, MCP server for AI agents |
If you only need political ads or EU transparency data, Meta's API is free and official, use it. If you want to see what a brand is running in the US, Brazil or anywhere else, read on.
The endpoints
| Endpoint | What it's for | Required |
|---|---|---|
/facebook/ads/search | Ads by keyword or by advertiser, with country, active / inactive, date and sort filters | query or ad_page_id |
/facebook/ads/keywords | Find advertisers for a keyword (gives their page_id) | query |
/facebook/ads/page-details | An advertiser's Ads Library profile | page_id |
/facebook/ads/archive-details | One ad's full details | ad_archive_id (and pass its page_id) |
/facebook/ads/countries | Supported country codes | — |
Each call costs 1 credit. The free plan includes 200 calls a month (pricing).
Search ads by keyword
1curl -G "https://api.socialapis.io/facebook/ads/search" \
2 -H "x-api-token: $SOCIALAPIS_API_KEY" \
3 --data-urlencode "query=running shoes" \
4 --data-urlencode "country=US" \
5 --data-urlencode "activeStatus=Active" \
6 --data-urlencode "sort_data=impressions"| Parameter | Values |
|---|---|
query | any keyword |
ad_page_id | an advertiser's ID, instead of query |
country | ISO code (US, GB, DE…) or ALL |
activeStatus | Active, Inactive or ALL |
after_time, before_time | YYYY-MM-DD |
sort_data | impressions or recent |
end_cursor | next page, from page_info.end_cursor |
Search one competitor's ads
You need the advertiser's ad_page_id. Two ways to get it:
- From their page URL:
/facebook/pages/detailswithlink=https://www.facebook.com/<brand>returns the page'sad_page_id. - From a keyword:
/facebook/ads/keywordswithquery=<brand name>lists matching advertisers and theirpage_id.
Then:
1curl -G "https://api.socialapis.io/facebook/ads/search" \
2 -H "x-api-token: $SOCIALAPIS_API_KEY" \
3 --data-urlencode "ad_page_id=<the page id>" \
4 --data-urlencode "country=ALL" \
5 --data-urlencode "activeStatus=Active"Reading the response
Two things trip people up the first time:
1. Ads come in groups. data.ads is a list of groups, and each group is a list of near-identical ads (the same creative run several times). Take the first ad of each group unless you need every copy:
1{
2 "data": {
3 "page_info": { "end_cursor": "AQHT…", "has_next_page": true },
4 "ads": [
5 [
6 {
7 "ad_archive_id": "1339489931715040",
8 "page_id": "71740214065",
9 "page_name": "KURU Footwear",
10 "is_active": true,
11 "start_date": 1783321200,
12 "end_date": 1793084400,
13 "publisher_platform": ["FACEBOOK", "INSTAGRAM", "AUDIENCE_NETWORK"],
14 "spend": null,
15 "snapshot": {
16 "display_format": "DCO",
17 "title": "{{product.name}}",
18 "body": { "text": "{{product.brand}}" },
19 "cta_text": "Shop now",
20 "link_url": "https://www.kurufootwear.com/pages/outbreak",
21 "cards": [
22 {
23 "title": "Get Comfort in the Chaos",
24 "body": "You saw the apocalypse. The zombies were scary. Now meet the shoes that make foot pain the real casualty…",
25 "link_url": "https://www.kurufootwear.com/pages/outbreak"
26 }
27 ]
28 }
29 }
30 ]
31 ]
32 }
33}2. Catalog ads have placeholders. Dynamic product ads (display_format DPA or DCO) show template text like {{product.name}} in snapshot.title and snapshot.body.text. The real copy is in snapshot.cards: use the first card's title and body for those.
Dates are Unix seconds. spend, impressions and reach are null for most commercial ads, because Meta doesn't publish them outside political and EU ads; no tool can give you a competitor's real budget.
Track a competitor's new ads (Python)
This checks one or more advertisers once a day, remembers which ads it has seen, and prints (or sends) only the new ones.
1import json
2import os
3import re
4from datetime import datetime, timezone
5from pathlib import Path
6
7import requests
8
9API_KEY = os.environ["SOCIALAPIS_API_KEY"]
10ADVERTISERS = [p.strip() for p in os.environ["AD_PAGE_IDS"].split(",") if p.strip()]
11SEEN_FILE = Path("seen_ads.json")
12PLACEHOLDER = re.compile(r"\{\{.*?\}\}")
13
14
15def search_ads(ad_page_id):
16 response = requests.get(
17 "https://api.socialapis.io/facebook/ads/search",
18 headers={"x-api-token": API_KEY},
19 params={"ad_page_id": ad_page_id, "country": "ALL", "activeStatus": "Active"},
20 timeout=90,
21 )
22 response.raise_for_status()
23 groups = response.json().get("data", {}).get("ads", [])
24 return [group[0] for group in groups if group]
25
26
27def ad_copy(ad):
28 snap = ad.get("snapshot") or {}
29 card = (snap.get("cards") or [{}])[0]
30 title = snap.get("title") or ""
31 text = (snap.get("body") or {}).get("text") or ""
32 if not title or PLACEHOLDER.search(title):
33 title = card.get("title") or ""
34 if not text or PLACEHOLDER.search(text):
35 text = card.get("body") or ""
36 return title, " ".join(text.split())[:300]
37
38
39def main():
40 seen = set(json.loads(SEEN_FILE.read_text())) if SEEN_FILE.exists() else set()
41 first_run = not seen
42
43 for page_id in ADVERTISERS:
44 for ad in search_ads(page_id):
45 ad_id = ad["ad_archive_id"]
46 if ad_id in seen:
47 continue
48 seen.add(ad_id)
49 if first_run:
50 continue
51 title, text = ad_copy(ad)
52 started = datetime.fromtimestamp(ad["start_date"], timezone.utc).date()
53 print(f"New ad from {ad['page_name']} (since {started}): {title}\n{text}\n"
54 f"https://www.facebook.com/ads/library/?id={ad_id}\n")
55
56 SEEN_FILE.write_text(json.dumps(sorted(seen)))
57
58
59if __name__ == "__main__":
60 main()Run it once by hand (the first run records the ads already live), then daily with cron, and replace the print with a Slack webhook, an email or a Google Sheets row. One advertiser checked daily is about 30 calls a month.
The same without code (n8n)
In n8n: Schedule Trigger (daily) → HTTP Request (GET https://api.socialapis.io/facebook/ads/search, Header Auth x-api-token, query ad_page_id or query, country, activeStatus=Active) → Set a field ads = {{ $json.data.ads.map(group => group[0]) }} → Split Out ads → Remove Duplicates (Remove items processed in previous executions, value {{ $json.ad_archive_id }}) → Google Sheets append and/or Slack. Add an AI step (Gemini, OpenAI…) before Slack to summarize the hooks and calls to action across the new ads.
With an AI assistant
The SocialAPIs MCP server exposes all of this as the facebook_ads tool, so in Claude, ChatGPT or Cursor you can ask "what ads is KURU running right now, and what hooks do they use?" and the assistant calls the Ads Library for you.
Is this allowed?
The Ads Library exists so anyone can see who is advertising what; the data here is what the public Ad Library website shows, without logging in. Use it for research, monitoring and analysis; don't present a competitor's creative as your own, and keep an eye on the terms of the platforms you republish to.
Common questions
Can I see how much a competitor spends? No. Meta only publishes spend ranges for political / issue ads and some EU data; for other ads spend is null everywhere. Long-running ads and many variants of one creative are the usual signals of what's working.
Does it include Instagram ads? Yes. Ads that run on Instagram appear with INSTAGRAM in publisher_platform, along with Facebook, Messenger, Audience Network and Threads.
Inactive ads? Set activeStatus=Inactive or ALL, and use after_time / before_time for a date range.
How do I open the ad on Facebook? https://www.facebook.com/ads/library/?id=<ad_archive_id>.
Get an API key and try it on your own competitors: the free plan includes 200 calls a month (sign up).