Install
openclaw skills install @scavio-ai/meta-ad-library-apiMeta Ad Library search by keyword, Facebook Page id or ad archive id, returning the full ad creative, run dates, the Meta platforms each ad ran on, and spend, reach and impressions on political and issue ads. Cursor pagination walks every ad, not just page one. 3 endpoints, 1 credit per page, structured JSON.
openclaw skills install @scavio-ai/meta-ad-library-apiSearch the Meta Ad Library by keyword, list every ad a Facebook Page is running, or pull one ad by its archive id - with the full creative, the platforms each ad ran on, its run dates, and spend/reach/impressions on political and issue ads. All three endpoints return structured JSON.
The paths are hyphenated: /api/v1/meta-ads/*. Copy them exactly.
Use this skill when the user asks to:
Most Ad Library tools show you a page and stop. This one walks the whole thing.
Page 1 returns 30 ads. After that, next_cursor gets you 10 ads per page for as long as has_next_page is true - across an entire keyword query or an entire advertiser. That is the differentiator over the $79-399/month ad-spy tools.
Each page is 1 credit, so a deep crawl is real but the cost scales with depth: roughly 10 ads per credit past the first 30. Tell the user the budget before starting a walk.
Get a free API key at scavio.dev (50 free credits to get started, no card required):
export SCAVIO_API_KEY=sk_live_your_key
Every request is a POST with a JSON body and:
Authorization: Bearer $SCAVIO_API_KEY
Base URL: https://api.scavio.dev. All paths are under /api/v1/meta-ads - hyphenated. Every endpoint costs 1 credit per page.
| Endpoint | Credits | What it returns |
|---|---|---|
POST /api/v1/meta-ads/search | 1 per page | Keyword search: 30 ads on page 1, then 10 per cursor page |
POST /api/v1/meta-ads/advertiser | 1 per page | Every ad from one Facebook Page id, same paging |
POST /api/v1/meta-ads/ad | 1 | One ad in full by archive id. No pagination. |
/meta-ads/search with query and a country./meta-ads/advertiser with the advertiser's numeric Facebook Page id (page_id)./meta-ads/ad with ad_archive_id.next_cursor back as cursor while has_next_page is true.country or media_type mid-walk does nothing; start a new walk instead.total_results caps at 50000 with total_is_capped: true. Meta itself only reports ">50,000". Never present a capped total as an exact count./search)| Parameter | Type | Default | Description |
|---|---|---|---|
query | string | required | Keyword or brand term (1-200 chars) |
country | string | US | Exactly two characters |
active_status | string | all | all, active, inactive |
ad_type | string | all | all, political_and_issue_ads |
media_type | string | -- | all, image, video, meme, image_and_meme, none |
search_type | string | keyword_unordered | keyword_unordered or keyword_exact_phrase |
cursor | string | -- | next_cursor from the previous response. All other filters are ignored when this is present. |
/advertiser)| Parameter | Type | Default | Description |
|---|---|---|---|
page_id | string | required | The advertiser's numeric Facebook Page id (3-25 digits) |
country | string | US | Exactly two characters |
active_status | string | all | all, active, inactive |
ad_type | string | all | all, political_and_issue_ads |
media_type | string | -- | all, image, video, meme, image_and_meme, none |
cursor | string | -- | Same rules as search |
/ad)| Parameter | Type | Default | Description |
|---|---|---|---|
ad_archive_id | string | required | The ad's numeric archive id (3-25 digits) |
Political and issue ads carry spend, reach, impressions and the paid-for-by disclosure. Commercial ads leave all of those null. That is Meta's disclosure regime, not a gap in the data.
To surface them, set ad_type: political_and_issue_ads. If the user is asking "how much is this brand spending on Facebook ads", the honest answer for a commercial advertiser is that Meta does not publish it.
import requests
BASE = "https://api.scavio.dev"
# Your key from https://scavio.dev/?utm_source=clawhub&utm_medium=skill&utm_campaign=meta-ad-library-api. Load it from your environment or secret
# store in real code - keep it out of source control.
API_KEY = "sk_your_key_here"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# 1. Keyword search - 30 ads on page 1
ads = requests.post(f"{BASE}/api/v1/meta-ads/search", headers=HEADERS,
json={"query": "project management software", "country": "US",
"active_status": "active", "media_type": "video"}).json()
# 2. Everything one Page is running, by numeric page id
advertiser = requests.post(f"{BASE}/api/v1/meta-ads/advertiser", headers=HEADERS,
json={"page_id": "20531316728", "country": "US"}).json()
# 3. One ad by archive id
ad = requests.post(f"{BASE}/api/v1/meta-ads/ad", headers=HEADERS,
json={"ad_archive_id": "1234567890123456"}).json()
# 4. Political ads, where spend and reach are actually published
political = requests.post(f"{BASE}/api/v1/meta-ads/search", headers=HEADERS,
json={"query": "climate", "country": "US",
"ad_type": "political_and_issue_ads"}).json()
Walking a whole query. 1 credit per page, 30 ads then 10 per page - cap it:
def crawl(path, body, max_pages=10):
"""1 credit per page. Page 1 = 30 ads, then 10 each: 10 pages ~= 120 ads, 10 credits."""
cursor, pages = None, []
for _ in range(max_pages):
# The cursor carries the filters. Sending them again changes nothing.
payload = {"cursor": cursor} if cursor else body
data = requests.post(f"{BASE}/api/v1/meta-ads/{path}",
headers=HEADERS, json=payload).json()["data"]
pages.append(data)
if not data.get("has_next_page"):
break
cursor = data["next_cursor"]
return pages
pages = crawl("search", {"query": "project management software", "country": "US"})
Every response uses the envelope { data, response_time, credits_used, credits_remaining }.
total_results, total_is_capped, has_next_page and next_cursor./api/v1/meta-ads/search, not /api/v1/metaads/search.total_results as exact when total_is_capped is true - Meta only reports ">50,000".null spend or reach as zero spend. Meta publishes those figures for political and issue ads only.has_next_page: false, not on a short page.The error set here is 400 / 401 / 404 / 429 / 502 - there is no 503 on this platform.
400 means an invalid or missing parameter - e.g. a non-numeric page_id or ad_archive_id, or a country that is not exactly two characters. Fix and retry.401 means the API key is invalid or missing. Check SCAVIO_API_KEY.404 means the ad or page does not resolve.429 means rate or usage limit exceeded. Wait before retrying. See https://scavio.dev/docs/rate-limits?utm_source=clawhub&utm_medium=skill&utm_campaign=meta-ad-library-api.502 means upstream is temporarily unavailable - wait a few seconds and retry.keyword_unordered rather than keyword_exact_phrase, or widen country and active_status.SCAVIO_API_KEY is not set, prompt the user to export it before continuing.langchain-scavio has no Meta Ad Library tool - use the Scavio SDK directly:
pip install scavio==0.15.0
from scavio import ScavioClient
client = ScavioClient() # reads SCAVIO_API_KEY
page1 = client.meta_ads.search("project management software", country="US",
active_status="active")
page2 = client.meta_ads.search("project management software",
cursor=page1["data"]["next_cursor"])
by_page = client.meta_ads.advertiser("20531316728", country="US")
one = client.meta_ads.ad("1234567890123456")
JavaScript / TypeScript:
npm install scavio@0.15.0
import { Scavio } from "scavio";
const scavio = new Scavio(); // reads SCAVIO_API_KEY
const ads = await scavio.metaAds.search({ query: "project management software", country: "US" });
const { next_cursor } = ads.data as Record<string, any>;
const next = await scavio.metaAds.search({ query: "project management software",
cursor: next_cursor });