Concepts
Ads
Searching Meta's Ad Library with its filters, what each Ad carries, the winner Score, Ad analysis, what is stored, and how an ad search is priced.
POST /v1/ads/search reads Meta’s Ad Library or TikTok’s, by keyword (query) or by advertiser (advertiser), exactly one of the two. The two libraries behave differently, because only Meta Ads are stored and kept over time.
Meta: filters
Leave a filter out to get its default. The defaults match Meta’s own library UI, so a plain keyword search returns running commercial Ads from every country.
| Filter | Values | Default |
|---|---|---|
country | a two-letter country code | every country |
status | active, inactive, all | active |
kind | commercial, political, issue, all | commercial |
media_type | video, image, carousel | every format |
placements | facebook, instagram, messenger, audience_network | every placement |
started_after, started_before | ISO dates or timestamps | no bound |
POST /v1/ads/search·Meta: $0.001 per Ad delivered ($0.00025 when served from a recent advertiser observation), plus $0.02 per Ad whose analysis is computed with `analyze`; the estimate quotes `limit` Ads, all analysed fresh up to the cap with `analyze`. TikTok: $0.02 per page.
curl -X POST "https://www.shortsintel.com/v1/ads/search" \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"library":"meta","query":"cold plunge","country":"US","status":"active","kind":"creator","media_type":"video","placements":["facebook"],"started_after":"started_after","started_before":"started_before","limit":20,"order_by":"relevance","analyze":true}'const response = await fetch(
"https://www.shortsintel.com/v1/ads/search",
{
method: "POST",
headers: {
"Authorization": "Bearer sk_live_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"library": "meta",
"query": "cold plunge",
"country": "US",
"status": "active",
"kind": "creator",
"media_type": "video",
"placements": [
"facebook"
],
"started_after": "started_after",
"started_before": "started_before",
"limit": 20,
"order_by": "relevance",
"analyze": true
}),
},
);
if (!response.ok) {
throw new Error(`ShortsIntel ${response.status}`);
}
const data = await response.json();import requests
response = requests.post(
"https://www.shortsintel.com/v1/ads/search",
headers={
"Authorization": "Bearer sk_live_YOUR_KEY",
"Content-Type": "application/json",
},
json={
"library": "meta",
"query": "cold plunge",
"country": "US",
"status": "active",
"kind": "creator",
"media_type": "video",
"placements": [
"facebook",
],
"started_after": "started_after",
"started_before": "started_before",
"limit": 20,
"order_by": "relevance",
"analyze": True,
},
)
response.raise_for_status()
data = response.json()200 · The Run finished inside the wait budget: `{ run, result }`. With `estimate=true`, `{ estimate_usd, cached }` instead.
{
"run": {
"id": "run_8f2c1ad04b",
"kind": "ads_search",
"status": "completed",
"created_at": "2026-09-08T09:14:02.418Z",
"completed_at": "2026-09-08T09:14:04.930Z",
"charge_usd": 0.002,
"list_price_usd": 0.002,
"cached": false
},
"result": {
"library": "meta",
"items": [
{
"library": "meta",
"id": "1122334455667788",
"url": "https://www.facebook.com/ads/library/?id=1122334455667788",
"advertiser": {
"id": "100064000000001",
"name": "Plunge"
},
"is_active": true,
"first_shown": "2026-08-01T00:00:00.000Z",
"last_shown": null,
"placements": [
"FACEBOOK",
"INSTAGRAM"
],
"creative": {
"title": "Cold water, on your schedule",
"body": "The tub our testers refused to give back.",
"cta": "Shop Now",
"link_url": "https://plunge.example/tub",
"format": "VIDEO",
"image_urls": [],
"video_urls": [
"https://video.xx.fbcdn.net/ads/plunge-tub-hd.mp4"
],
"thumbnail_urls": []
},
"reach": {
"impressions": null,
"spend": null,
"currency": null,
"audience": null
},
"kind": "commercial",
"media_type": "video",
"countries": null,
"variant_count": 4,
"days_running": 72,
"winner_score": {
"score": 88,
"confidence": "high",
"inputs": {
"days_running": 72,
"variants_added": 2,
"snapshots": 6,
"advertiser_ads": 14
},
"reason": null
}
},
{
"library": "meta",
"id": "2233445566778899",
"url": "https://www.facebook.com/ads/library/?id=2233445566778899",
"advertiser": {
"id": "100064000000001",
"name": "Plunge"
},
"is_active": false,
"first_shown": "2026-06-02T00:00:00.000Z",
"last_shown": "2026-07-14T00:00:00.000Z",
"placements": [
"INSTAGRAM"
],
"creative": {
"title": "Three minutes. Every morning.",
"body": "Our coldest tub yet, now in matte black.",
"cta": "Learn More",
"link_url": "https://plunge.example/black",
"format": "IMAGE",
"image_urls": [
"https://scontent.xx.fbcdn.net/ads/plunge-black.jpg"
],
"video_urls": [],
"thumbnail_urls": []
},
"reach": {
"impressions": null,
"spend": null,
"currency": null,
"audience": null
},
"kind": "commercial",
"media_type": "image",
"countries": null,
"variant_count": null,
"days_running": 42,
"winner_score": {
"score": 54,
"confidence": "low",
"inputs": {
"days_running": 42,
"variants_added": 0,
"snapshots": 1,
"advertiser_ads": 14
},
"reason": "single_snapshot"
}
}
],
"next_cursor": "eyJjIjoiQVpVWiIsIm8iOjB9",
"total": null
}
}Meta: limit and paging
limit defaults to 50 and goes up to 500. Meta serves its library 10 to 30 Ads at a time, so one call reads as many library pages as it takes to fill your limit. Pass next_cursor back as cursor for more, with the same filters. A large limit can take a while: with a short wait you get the Run id back to poll.
Meta: by advertiser
advertiser takes the advertiser’s Facebook Page id, its Page URL, or its name. A Page id (or a URL with one in it) is never ambiguous. A name several Pages carry is answered with 409 advertiser_ambiguous and a list of candidates (Page id, name, Page URL, linked Instagram account, Ad count when known) instead of a guess; ask again with the right page_id. That answer costs nothing.
A search with status: all and no other filter that reads to the end of the advertiser’s Ads is a complete observation of it. For six hours afterwards, any search on that advertiser (without country) is answered from what was stored, at $0.00025 per Ad instead of $0.001. Every advertiser search says cached and as_of, when its Ads were observed.
GET /v1/advertisers/{page_id} is the advertiser’s profile: name, Page URL, its linked Instagram account and Creator (only when Meta shows the link), running and total Ad counts as last observed, and when. $0.02 fresh; $0.005 within six hours of a complete observation. A fresh lookup reads the advertiser’s Ads and stores them, so it is a complete observation itself when the advertiser runs few enough Ads to read in one go.
Meta: what each Ad carries
id, url: Meta's library id, and the Ad's page in the library.advertiser: The Facebook Page running the Ad: Page id and name.kind, media_type: Commercial, political or issue; video, image or carousel.creative: Copy, title, call to action, landing URL and media URLs.first_shown, last_shown, is_active, days_running: When it started, when it stopped (only once it has), whether it is running, and whole days run so far.placements, countries, variant_count: Where it ran, the countries Meta discloses, and how many Ads share its media.winner_score: How strongly the Ad looks like a winner, 0 to 100, with its confidence, inputs and reason: see below.reach: Spend, impressions and reach ranges exactly as Meta discloses them: for political and issue Ads, and reach for some EU Ads. Null otherwise.
Ads are trimmed for an agent’s context: you never get the library’s raw payload. Each Ad is also one item of the Run, listed by GET /v1/runs/{id}/items with type=ads.
Meta: winner Score and ordering
Meta discloses no spend for commercial Ads, so the winner Score reads what the advertiser does instead: an Ad they keep running, and keep cloning into new variants, is one that pays back. It is computed from the stored history of the Ad, never fetched, and measured against the same advertiser’s other stored Ads, because a long run means something different for a brand that rotates creative weekly.
days_running: whole days from start to stop, or to now while running.variants_added: the variant count at the latest snapshot minus at the first.- Each is ranked against the advertiser’s other Ads: the share below it, ties counting half.
scoreis 100 × (0.7 × the days rank + 0.3 × the variants rank), rounded. - An Ad seen once has no variant history yet: its score is the days rank alone,
confidence: low, reasonsingle_snapshot. - With no other dated Ad of the advertiser, the ranks fall back to days out of 90 and variants out of 5. Under 5 dated Ads of the advertiser the score is low confidence, reason
few_advertiser_ads. No start date: no score, reasonno_start_date.
order_by takes relevance, winner, days_running, start_date. relevance, the default, keeps the library’s order; the others put the highest score, longest run or newest start first, with unknowns last. It sorts the Ads a call returns: next_cursor still follows the library’s order.
Meta: analysis
Pass analyze: true on a search, or analyze=true on GET /v1/ads/meta/{library_id}, and each Ad comes back with intelligence. A video Ad gets the full video set (transcript, hook, call to action, sponsorship, tone, visual format, brand safety), pipeline: video. An image or carousel Ad gets what an image can carry: copy angle, offer, call to action, visual format, topics and sentiment, pipeline: image. Both read our archived copy of the media, with the Ad’s copy as context; the copy is never analysed on its own, so an Ad whose media cannot be read fails with media_unavailable.
Analysis is computed once per Ad and version and reused by every later call, yours or anyone’s. Computing it costs $0.02 per Ad on top of delivery; reusing it costs nothing extra, and every Ad carries the analysis already stored for it even without analyze. A search analyses its first 50 Ads. The analysis runs after the Ads are read, so expect a 202 and poll the Run. An Ad whose analysis fails is not charged at all and makes the Run partial; if no Ad gets an analysis the Run fails and costs nothing. Read stored analysis free at GET /v1/ads/meta/{library_id}/intelligence.
Meta: what is stored
Every Ad a search returns is stored with its advertiser, together with a snapshot of its public state at that moment: running or stopped, placements, countries, variant count, and any disclosed spend or reach. That happens whoever asked, so the history of an Ad builds up over time. A keyword search is always read live from the library, never served from what is stored; an advertiser search can be, as above.
Meta: price
$0.001 per Ad delivered, not per page, plus $0.02 per Ad whose analysis is computed with analyze. An estimate quotes your limit in Ads (all analysed fresh, up to the cap, with analyze), the most the search can cost; the Run charges for the Ads it actually returned. If the library fails part-way, the Run is partial, charged only for the Ads already delivered, and incomplete says what stopped it; next_cursor resumes from there. If the library cannot be reached at all, the Run fails with source_unavailable and costs nothing, and an estimate answers source_unavailable instead of a price. An outage is never an empty list. Every price is on the pricing page.
TikTok
The TikTok library is one page per call: limit defaults to 25 and goes up to 30, at $0.02 per page. It is global and nothing from it is stored, so it refuses country and every Meta filter rather than quietly ignoring them.