Install
openclaw skills install @scavio-ai/scavio-meta-adsSearch the Meta Ad Library by keyword or Facebook Page id and walk full cursor pagination through every ad, with creative, run dates, platforms and political spend. 3 endpoints, 1 credit per page.
openclaw skills install @scavio-ai/scavio-meta-adsSearch 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 https://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 os, requests
BASE = "https://api.scavio.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['SCAVIO_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.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
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
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 });