Meta Ad Library
Meta Ad Library Search API
Search the Facebook and Instagram ad library by keyword and get every ad back as JSON: advertiser page, ad copy, headline, CTA, the image and video creative, which platforms it ran on and its run dates. AND IT PAGES ALL THE WAY DOWN - 30 ads on page 1, then 10 per cursor page, walking has_next_page to the end of the query. Public, logged-out data, no Meta app review and no access token. Costs 1 credit per page.
Authorizations
AuthorizationstringheaderrequiredBearer authentication header of the form Bearer <token>, where <token> is your Scavio API key (e.g. Bearer sk_live_your_key).
Body
application/jsonquerystringrequiredThe keyword to search the ad library for, 1-200 characters. Matched against ad copy and advertiser names, in any order by default - see search_type for exact-phrase matching.
Example: nike
countrystringdefault:USWhich country's ad library to read, as an ISO 3166-1 alpha-2 code. EXACTLY 2 CHARACTERS - "USA" or "United States" is a 400. This is the country the ads were served in, not the advertiser's home country, and it changes the result set: the same query returns different ads for US and DE.
Example: GB
active_statusenum<string>default:allWhether the ad is still running.
all— Running and finished ads (default).active— Only ads still running today.inactive— Only ads that have stopped - the creative graveyard a competitor stopped paying for.
ad_typeenum<string>default:allWhich library to search. This is the ONLY way to get spend, reach and impressions: Meta publishes them for political and issue ads and for nothing else.
all— The full library (default). spend, reach_estimate, impressions and disclosure come back null on commercial ads - expected, not a bug.political_and_issue_ads— Political and issue ads only, with spend, reach_estimate, impressions, the paid-for-by byline and the disclosure populated.
media_typeenum<string>Restrict to one creative format. No default - unset means no media filter at all.
all— Explicitly no media filter.image— Image creatives.video— Video creatives.meme— Meme-format creatives.image_and_meme— Images and memes together.none— Ads with no media attached.
search_typeenum<string>default:keyword_unorderedHow the query is matched.
keyword_unordered— All words, any order (default).keyword_exact_phrase— The phrase exactly as written.
cursorstringThe next_cursor from your previous response. Page 1 returns 30 ads; every cursor page after it returns 10. THE OTHER FILTERS ARE IGNORED WHEN A CURSOR IS PRESENT - the cursor is a self-contained blob that already carries the query, country and every filter from the call that produced it, so paging is stateless and you never re-send them. To change a filter, start a new search without a cursor.
Example: AQHSvXBYGcKWgG-6pkoHQCfi8ZzKOiGw189Q-JALUaalccncktmhfyriiU1M_ys0fJhg
Request
from scavio import ScavioClient
client = ScavioClient(api_key="sk_live_your_key")
# Walk the whole query. Page 1 is 30 ads, every cursor page after it is 10.
page = client.meta_ads.search(query="nike", country="US", active_status="active")
ads = list(page["data"]["ads"])
while page["data"]["has_next_page"]:
# Only the cursor is re-sent: it already carries query, country and filters.
page = client.meta_ads.search(query="nike", cursor=page["data"]["next_cursor"])
ads.extend(page["data"]["ads"])
print(len(ads), "ads")
for ad in ads[:5]:
print(ad["page_name"], ad["cta_text"], ad["link_url"])Response