Meta's Ad Library is the single best public window into what your competitors are actually spending money on. Every active ad on Facebook and Instagram is there — the creative, the call-to-action, the landing domain, the day it launched. The problem is the interface: it is built for one-off manual lookups, not for tracking fifty advertisers across three markets every week. You cannot diff last month's creative against this month's, you cannot sort by how long an ad has been running, and you certainly cannot pipe it into a dashboard.

This guide shows how to read the same public Ad Library programmatically: full-text ad search with real filters, advertiser profiles, and a strategy-insights endpoint that surfaces launch cadence, format mix, and the winning creatives a brand refuses to turn off. Every call is a single authenticated request that returns clean JSON — no headless browser, no proxy pool, no HTML parsing.

Three endpoints, one job: read a competitor's ad strategy

The Meta Ads surface has three endpoints. Search is your discovery tool; the other two go deep on a single advertiser once you have their page_id. Every response comes back in the standard { data, meta } envelope, where meta carries the exact cost, your remaining balance, and a request_id for support.

EndpointMethodWhat it returnsCost
/v1/meta_ads/searchPOSTFull-text ad search: total + up to 200 ads (creatives, CTA, domain, format, startDate, daysActive, isActive)$0.0010
/v1/meta_ads/advertiserGETPage profile: page_name, category, likes, ads_seen, ads_active$0.0002
/v1/meta_ads/insightsGETStrategy aggregates: format/CTA/platform distributions, launch cadence by month, longest-running active ads$0.0010

You never pay for a miss

Billing only happens on a 200. If an advertiser has no ads or a page_id does not resolve, the 404/5xx costs you nothing — so you can probe aggressively while building a watchlist.

Search: full-text ad discovery with filters

POST /v1/meta_ads/search takes a JSON body — not query params — because there are a lot of optional filters and you rarely use all of them at once. Send a query for full-text keyword search, or scope to a known advertiser, linkDomain, platform, mediaType, or ctaType. The key strategic lever is sortBy: set it to daysActive to float a brand's longest-running (read: best-performing) creatives to the top.

shell
curl -X POST "https://api.virev.ai/v1/meta_ads/search" \
  -H "Authorization: Bearer $VIREV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "protein",
    "activeOnly": true,
    "minDaysActive": 30,
    "sortBy": "daysActive",
    "limit": 50
  }'

The same request in JavaScript — the body maps one-to-one to the fields above. Filter by advertiser when you already know who you are watching, or by linkDomain to catch every brand driving traffic to a specific store or funnel.

search-competitor-ads.js
const res = await fetch("https://api.virev.ai/v1/meta_ads/search", {
  method: "POST",
  headers: {
    "Authorization": "Bearer kb_live_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    advertiser: "Gymshark",
    platform: "instagram",
    mediaType: "video",
    activeOnly: true,
    sortBy: "daysActive",
    limit: 100,
  }),
});

const { data, meta } = await res.json();
console.log(`${data.total} ads found — charged ${meta.cost}`);

for (const ad of data.ads) {
  console.log(ad.pageName, ad.ctaType, ad.linkDomain, `${ad.daysActive}d live`);
}

Run it live — edit the query to search your competitor's ads

Meta Ad Library search POST
$ curl -X POST api.virev.ai/v1/meta_ads/search -d '{"query":" "}'
live · real API call · $0.0010

Longevity is your performance proxy

The Ad Library does not publish spend or impressions for commercial ads, so you cannot read a winner off a metric. Instead, read it off time: a creative a brand has paid to keep live for 90+ days has survived their own optimization loop. Sort by daysActive, set minDaysActive, and the ads at the top are the ones already working.

Advertiser: the cheapest lookup you have

Once search hands you a page_id, GET /v1/meta_ads/advertiser?page_id= gives you the page in profile form: page_name, category, likes, ads_seen, and ads_active. At $0.0002 it is the cheapest call in the catalog — ideal for a nightly sweep that flags when a competitor's ads_active count jumps, a reliable early signal of a new campaign or seasonal push.

advertiser.py
import requests

res = requests.get(
    "https://api.virev.ai/v1/meta_ads/advertiser",
    params={"page_id": "20531316728"},
    headers={"Authorization": "Bearer kb_live_..."},
)

payload = res.json()
advertiser = payload["data"]
print(advertiser["page_name"], "-", advertiser["category"])
print("active ads:", advertiser["ads_active"], "/ seen:", advertiser["ads_seen"])
print("charged:", payload["meta"]["cost"])

Insights: the whole strategy in one call

GET /v1/meta_ads/insights?page_id= is the report a strategist would spend an afternoon assembling by hand. In one $0.0010 request it aggregates every ad the page has run into: total_ads and active_ads, avg_days_active and max_days_active, a display_format_distribution (video vs. image vs. carousel), a cta_type_distribution, a publisher_platform_distribution (Facebook vs. Instagram vs. Audience Network), ads_started_by_month for launch cadence, and — most importantly — the list of the longest-running active ads.

insights.py
import requests

res = requests.get(
    "https://api.virev.ai/v1/meta_ads/insights",
    params={"page_id": "20531316728"},
    headers={"Authorization": "Bearer kb_live_..."},
)

insights = res.json()["data"]
print("total ads:", insights["total_ads"], "| active:", insights["active_ads"])
print("format mix:", insights["display_format_distribution"])
print("cta mix:", insights["cta_type_distribution"])
print("launch cadence:", insights["ads_started_by_month"])

# The winning creatives: ads they refuse to turn off
for ad in insights["longest_running_active_ads"]:
    print(ad["daysActive"], "days:", ad["ctaType"], "->", ad["linkDomain"])

Read those distributions like a media plan. A display_format_distribution skewed 80% to video tells you where a competitor is putting their creative budget. A cta_type_distribution dominated by SHOP_NOW versus SIGN_UP tells you whether they are optimizing for direct sales or list-building. And ads_started_by_month reveals their tempo — a spike in Q4 is a seasonal war chest you can plan around.

A repeatable competitor-tracking loop

Put the three endpoints together and you have a workflow you can run on a schedule instead of clicking through the Ad Library by hand.

  1. 1

    Discover

    Call POST /v1/meta_ads/search with advertiser (or a category query) and sortBy: "daysActive" to pull each competitor's live ads and capture their pageName and page_id.
  2. 2

    Snapshot cheaply

    Run GET /v1/meta_ads/advertiser?page_id= nightly at $0.0002 each and store ads_active. Alert when the count changes — that is a new campaign going live.
  3. 3

    Go deep on movers

    When a page's activity jumps, call GET /v1/meta_ads/insights?page_id= for the full format/CTA/platform breakdown and the longest-running creatives to reverse-engineer.
  4. 4

    Diff over time

    Persist each run and compare weeks. New linkDomain values mean new funnels; a rising avg_days_active means their creatives are getting stickier — and worth studying.

Watch the rate limit on wide sweeps

The default limit is 100 requests/minute, surfaced in the X-RateLimit-* response headers. If you are snapshotting hundreds of advertisers, batch them under that ceiling — a 429 returns a Retry-After header telling you exactly how long to back off (and, like every non-200, costs nothing).

Start tracking competitor ads in one request

Pay $1, get a $5 balance — enough for roughly 5,000 ad searches or 25,000 advertiser lookups. No subscription.

Get your API key →