adlibrary.com Logoadlibrary.com
Share
Guides & Tutorials,  Platforms & Tools

Python Ad Library API Scripts: A Working Cookbook

Five short Python scripts cover 90% of programmatic ad research on the AdLibrary API: resolve a brand to its advertiser IDs, search keywords into pandas, curate a watchlist, score winners, and enrich the survivors — with the credit math and 429/402 handling already written.

Python ad library API scripts automate multi-platform ad research with a shared Python client and pandas

Most programmatic ad research dies in a Jupyter notebook called untitled_v3.ipynb. The fix is smaller than you think. Five short Python ad library API scripts cover roughly 90% of what marketers actually automate: resolve a brand to its advertiser IDs, search keywords into pandas, track a watchlist, score an advertiser's winners, and enrich the survivors. This cookbook gives you all five, working, against the AdLibrary API.

TL;DR: Five copy-paste Python scripts run the full ad-research loop on the AdLibrary API: brand resolution (free), filtered keyword search into a pandas DataFrame (1 credit per page), watchlist tracking with 30-minute curate sessions (1 credit per brand per session), winners scanning with tier parsing (10 credits flat, auto-refunded on failure), and batch enrichment with a local cache so you never pay twice for the same ad. Rate-limit and 402 handling included as code.

Every script below uses requests and pandas, real documented parameters, and one shared client. Adapt freely.

Why Python Ad Library API Scripts Beat Manual Research

You could click through ad libraries by hand. Plenty of teams still do, and for a one-off creative research session that's fine. The breakdown happens when research becomes recurring: ten competitors, four platforms, every Monday. Screenshots don't diff. Decks go stale in a week.

Meta's Ad Library API is the obvious free starting point, and credit where due: Meta invented the category. But it is Meta-only, scoped to political and social-issue ads in most regions, and gated behind app review plus an access token that expires every 60 days. Google's Ads Transparency Center and LinkedIn's Marketing API each add another auth model, another schema, another quota to babysit. We covered the free option's exact ceilings in Meta Ad Library free API in 2026, and why scraping tools are the wrong patch for them.

The AdLibrary API takes a different trade. It's paid, and it returns commercial ads across eleven platforms (Facebook, Instagram, TikTok, YouTube, Google, LinkedIn, Twitter, Pinterest and more) through one adl_ API key, with performance signals Meta's free endpoint never exposes: impressions, estimated spend, heat score, runtime. Meta's free API is fine for one platform. The moment you add TikTok, YouTube, or LinkedIn data into the same query, you need something else. That's the gap this Python ad library API cookbook fills. If you want the upgrade rationale in full, read Ad Library alternative with API access.

Setup: Environment, Auth, and a Shared Client

You need an API key, two libraries, and one small module everything else imports.

API keys are created in your dashboard on the Business plan (the key starts with adl_ and is shown exactly once, so store it immediately). There's no OAuth dance and no app review. You can hold up to 10 keys, which is handy for separating clients or environments. Details live on the API access feature page.

Set up a clean environment per the official venv docs:

bash
python -m venv .venv && source .venv/bin/activate
pip install requests pandas pyarrow
export ADLIBRARY_API_KEY="adl_your_key_here"

Then the shared client. Every later script imports call() from this file:

python
# adlib.py — shared client for all five scripts
import os
import time
import requests

BASE = "https://adlibrary.com"
HEADERS = {
    "Authorization": f"Bearer {os.environ['ADLIBRARY_API_KEY']}",
    "Content-Type": "application/json",
}

class InsufficientCredits(Exception):
    def __init__(self, body):
        self.body = body
        super().__init__(body.get("error", "Insufficient credits"))

def call(method, path, max_retries=4, **kwargs):
    for attempt in range(max_retries):
        r = requests.request(method, f"{BASE}{path}",
                             headers=HEADERS, timeout=90, **kwargs)
        if r.status_code == 429:
            wait = int(r.headers.get("Retry-After", "30"))
            time.sleep(wait)
            continue
        if r.status_code == 402:
            raise InsufficientCredits(r.json())
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"{path}: still rate-limited after {max_retries} tries")

Two deliberate choices here. The 429 branch honors the Retry-After header instead of hammering with a fixed backoff. And a 402 raises immediately rather than retrying, because insufficient credits is never transient — more on both in the error-handling section. The full endpoint surface is documented in the adlibrary API documentation and implementation guide.

Script 1: Resolve a Brand Name to Advertiser IDs

Everything downstream needs platform IDs, never brand names. Meta wants a numeric page ID, Google wants an AR… advertiser ID, LinkedIn wants a numeric company ID. The resolution endpoint is free, so this is always your first call:

python
# resolve.py — brand name -> cross-platform advertiser IDs (free)
from adlib import call

def resolve(brand, country="US"):
    data = call("GET", "/api/advertisers/search",
                params={"q": brand, "country": country, "limit": 10})
    best = data.get("best_match")
    if best:
        return {
            "name": best["name"],
            "confidence": best["confidence"],
            "meta_page_id": (best.get("meta") or {}).get("id"),
            "google_advertiser_id": (best.get("google") or {}).get("id"),
            "linkedin_company_id": (best.get("linkedin") or {}).get("id"),
        }
    # no cross-platform agreement: inspect candidates manually
    for platform in ("meta", "google", "linkedin"):
        for c in data["candidates"].get(platform, [])[:3]:
            print(platform, c.get("id"), c.get("name"))
    return None

if __name__ == "__main__":
    print(resolve("gymshark"))

The best_match object is only populated when at least two platforms agree on the normalized brand name, and confidence reflects that agreement (0.66 for two of three, 1.0 for all three). When it comes back None, don't guess. Print the candidates, eyeball them, pick the right IDs yourself. A wrong page ID fed into the winners scan in Script 4 returns real results for the wrong brand and still charges you.

Keep a small brands.json of resolved IDs in your repo. Resolution is free, but your scripts shouldn't re-resolve the same brand on every run when the IDs never change.

Script 2: Keyword Search With Filters, Straight Into pandas

The search endpoint is the workhorse of any Python ad library API workflow: one POST, every platform, 1 credit per page. The script below runs a filtered query and normalizes results into a pandas DataFrame, because ad research questions are DataFrame questions. Which advertiser shows up most? What's the median runtime of video ads in this niche? groupby answers both.

python
# search_df.py — filtered keyword search into pandas (1 credit/page)
import pandas as pd
from adlib import call

FORMATS = {1: "image", 2: "video", 3: "carousel", 4: "collection"}

def search_to_df(keyword, **filters):
    body = {"keyword": keyword, "appType": "3", **filters}
    data = call("POST", "/api/search", json=body)
    rows = [{
        "ad_key":      ad.get("ad_key"),
        "advertiser":  ad.get("advertiser_name"),
        "platform":    ad.get("platform"),
        "format":      FORMATS.get(ad.get("ads_type")),
        "text":        ad.get("message"),
        "impressions": ad.get("impression"),
        "likes":       ad.get("like_count"),
        "shares":      ad.get("share_count"),
        "comments":    ad.get("comment_count"),
        "first_seen":  pd.to_datetime(ad.get("first_seen"), unit="s"),
        "last_seen":   pd.to_datetime(ad.get("last_seen"), unit="s"),
    } for ad in data["results"]]
    df = pd.DataFrame(rows)
    if not df.empty:
        df["days_running"] = (df["last_seen"] - df["first_seen"]).dt.days
    print(f"{data['total']} total matches | "
          f"credits left: {data['_credits']['remaining']}")
    return df

df = search_to_df(
    "collagen gummies",
    platform=["facebook", "instagram"],
    adsType=["2"],            # video only
    daysBack=30,
    geo=["USA"],
    sortField="-impression",
)
df.to_parquet("cache/collagen_gummies.parquet")
print(df.groupby("advertiser")["days_running"].median().nlargest(10))

Three things to internalize. First, the response's total field is your market-size number, so you get sizing for free with the first page. Second, every page is a fresh 1-credit search and API keys get no same-keyword discount, which is why the last line writes a Parquet file — cache locally, re-query your own disk. Third, impression values are reach signals, with Meta reporting bucketed ranges rather than exact counts, so treat them as bands. If you want to turn impression bands into budget guesses, the ad spend estimator does that math, and the CPM calculator converts spend and impressions into comparable unit costs.

Useful sort fields beyond -impression: -days surfaces long-running creatives (the strongest proxy for "this converts", since ad fatigue kills the duds early) and -heat_degree ranks by momentum. Filters worth knowing: adsFormat for aspect ratio (9:16 for vertical), videoDurationBegin and videoDurationEnd for length, likeBegin for an engagement floor, and ecommercePlatform to study only Shopify stores.

Script 3: A Watchlist Tracker That Reuses Curate Sessions

Competitor ad monitoring is the highest-ROI automation in this cookbook. Save each brand once with its platform IDs, then pull every recent ad it runs through the curate endpoint: all platforms in one request, merged and deduped by ad key. One brand is rarely one account (Nike alone runs Nike, Nike Football, and Nike Run Club), and a saved advertiser holds all of its accounts behind a single call.

The billing model rewards code that understands sessions. The first curate call for an advertiser costs 1 credit and opens a 30-minute window. Continuation calls inside that window are free, provided your request carries the pagination cursors from the previous response. So the script persists cursors to disk and replays them:

python
# watchlist.py — save brands once, curate on a schedule
import json
import pathlib
from adlib import call

STATE = pathlib.Path("curate_state.json")

def save_brand(name, meta_ids=(), google_ids=(), linkedin_ids=()):
    body = {"name": name,
            "meta_page_ids": list(meta_ids),
            "google_advertiser_ids": list(google_ids),
            "linkedin_company_ids": list(linkedin_ids)}
    return call("POST", "/api/advertisers", json=body)["advertiser"]["id"]

def curate(advertiser_id):
    state = json.loads(STATE.read_text()) if STATE.exists() else {}
    cursors = state.get(advertiser_id)
    body = {"cursors": cursors} if cursors else {}
    data = call("POST", f"/api/advertisers/{advertiser_id}/curate", json=body)

    ads, new_cursors = [], {}
    for platform in ("meta", "google", "linkedin"):
        block = data.get(platform) or {}
        ads.extend(block.get("ads", []))
        if block.get("cursors"):
            new_cursors[platform] = block["cursors"]
    state[advertiser_id] = new_cursors
    STATE.write_text(json.dumps(state, indent=2))

    if "credits" in data:   # only present when this call was charged
        print(f"new session: 1 credit, {data['credits']['remaining']} left")
    return ads

if __name__ == "__main__":
    watchlist = json.loads(pathlib.Path("brands.json").read_text())
    for brand_id in watchlist.values():
        new_ads = curate(brand_id)
        print(brand_id, len(new_ads), "ads")

Design notes. Paginate within one run, while the 30-minute session is alive — a cursor replayed tomorrow starts a new session and a new charge, which is correct behavior, just worth budgeting for. The credits key only appears on charged calls, so its presence is your billing log. And if two processes hit the same advertiser at once, the loser gets a 409 with code: "CURATE_IN_PROGRESS" and a retryAfterSeconds hint of 15.

Diffing today's pull against yesterday's by ad_key turns this into a launch detector. The alerting half of that pipeline is covered in how to monitor competitor ads and the competitor ad monitoring setup guide.

Ad creative winners scoring dashboard ranking python ad library api results by performance tier

Script 4: Winners Scan With Tier Parsing

Searching tells you what a brand runs. The winners endpoint tells you what a brand has actually scaled, by scoring its whole portfolio and labeling each creative concept high_confidence_winner, winner, or loser. This is the most expensive call in the API at a flat 10 credits, so the script front-loads the free confirmation step:

python
# winners.py — confirm page (free), then scan (10 credits flat)
from adlib import call

def confirm_page(brand, country="US"):
    data = call("GET", "/api/winners/search-pages",
                params={"q": brand, "country": country})
    for p in data["pages"]:
        print(f"{p['page_id']:>16}  {p['page_name']:<30} "
              f"ads={p['ad_count']}  verified={p.get('verification')}")
    return data["pages"]

def scan_winners(page_id, country="US"):
    data = call("POST", f"/api/winners/advertiser/{page_id}",
                json={"country": country, "top_enrich": 100, "max_pages": 20})
    s = data["summary"]
    print(f"{s['page_name']}: {s['total_ads']} ads -> {s['tier_counts']}")

    winners = [r for r in data["results"]
               if r["score"]["tier"] in ("winner", "high_confidence_winner")]
    for r in sorted(winners, key=lambda w: -w["score"]["composite"]):
        sc = r["score"]
        print(f"\n[{sc['tier']}] composite={sc['composite']:.2f}")
        for reason in sc["reasons"]:
            print("  +", reason)
        for delta in (sc.get("dna_diff") or {}).get("deltas", []):
            print("  vs same-LP losers:", delta)
    return winners

Run confirm_page first, every time. The 10-credit fee auto-refunds when a scan finds no ads or hits an upstream error, but a valid page ID for the wrong brand returns real results and charges real credits. The refund logic can't read your intent.

The output is where this earns its fee. Each winner arrives with plain-language evidence in score.reasons ("Runtime 89 days, top 10% of this advertiser") and, in dna_diff.deltas, the concrete differences between the winning variant and losing variants pointing at the same landing page. That delta is competitive intelligence in its most actionable form: what the winner does that its own siblings don't. Results are deduped to concepts, so a brand running one idea in eight variants shows up as one row.

One constraint to respect in code: a single winners scan per account at a time. Fire a second concurrent scan and you get a 429 with Retry-After: 30. Serialize your watchlist scans in a plain loop rather than a thread pool.

Script 5: Batch Enrichment With a Local Cache

Enrichment is the AI analysis layer: point it at one ad and it returns a structured teardown — transcript, strategic read, persuasion analysis, plus a replication-ready creative brief. It costs 1 credit per ad for text and short video, with longer videos billed by duration. Batch it naively and you torch a month of credits in an afternoon. Batch it with caching and it stays cheap:

python
# enrich_batch.py — enrichment with a local cache + budget guard
import json
import pathlib
import time
from adlib import call

CACHE = pathlib.Path("enrichment_cache")
CACHE.mkdir(exist_ok=True)

def enrich(ad):
    cached = CACHE / f"{ad['ad_key']}.json"
    if cached.exists():
        return json.loads(cached.read_text())   # local hit: no API call at all
    data = call("POST", "/api/enrichment", json={"ad": ad})
    cached.write_text(json.dumps(data["enrichment"]))
    cost = 0 if data.get("alreadyUnlocked") else data.get("creditsUsed", 1)
    print(f"{ad['ad_key']}: {cost} credit(s), balance {data['balance']}")
    return data["enrichment"]

def enrich_batch(ads, credit_budget=25):
    spent, briefs = 0, []
    for ad in ads:
        if spent >= credit_budget:
            print(f"budget of {credit_budget} reached, stopping")
            break
        was_cached = (CACHE / f"{ad['ad_key']}.json").exists()
        briefs.append(enrich(ad))
        if not was_cached:
            spent += 1
            time.sleep(3)        # stay under 20 requests/min
    return briefs

The caching has three layers and it pays to know them. Your local file cache is layer zero and costs nothing. Server-side, alreadyUnlocked: true means you personally paid for this exact ad before, and the call is free forever after. A shared-cache hit (cached: true without alreadyUnlocked) skips the AI run but still charges 1 credit, because it's the first time you unlocked that ad. Only feed this function ads that survived a filter — the winners from Script 4, or the top decile of a Script 2 DataFrame by days_running. Enriching everything is how credit budgets die. The briefs that come back slot straight into a swipe file or your creative production queue.

Rate Limits and 402s, Handled in Code

The shared client already retries 429s. The part most scripts get wrong is the 402, so here is the explicit branch logic:

python
# errors.py — 402 handling: read the body, never blind-retry
from adlib import InsufficientCredits, call

def safe_call(method, path, **kwargs):
    try:
        return call(method, path, **kwargs)
    except InsufficientCredits as e:
        b = e.body
        if b.get("noPaymentMethod"):
            raise SystemExit("Auto-charge is on but no card is on file.")
        if b.get("needsCharge"):
            raise SystemExit(f"Balance {b.get('balance')} too low. "
                             "Add credits or upgrade, then re-run.")
        raise SystemExit(f"Out of credits: {b}")

A 402 is terminal, never transient. Retrying it on a timer fails identically forever and makes your logs lie to you. The body says which wall you hit: noPaymentMethod means auto-charge has nothing to bill, and needsCharge means the balance is plain empty. Both need a human, never a retry loop.

Rate limits worth encoding as constants: search allows 10 requests per minute per key and enrichment allows 20 per minute. The 429 body ships with a Retry-After header, and the client above honors it. For daily-volume planning, a key handles 10,000 requests per day, which is far more headroom than any research workflow in this post needs.

Putting the Scripts on a Schedule

Five working Python ad library API scripts become a research system the day they run without you. The simplest version is cron, and the pattern matches the one in how to build a Meta ads cron job:

bash
# crontab: curate the watchlist at 06:00, scan winners on Mondays
0 6 * * *  cd /opt/adresearch && .venv/bin/python watchlist.py >> logs/curate.log 2>&1
0 7 * * 1  cd /opt/adresearch && .venv/bin/python winners_weekly.py >> logs/winners.log 2>&1

A nightly watchlist run catches new creatives the morning after launch instead of in next month's audit. Narrow, frequent pulls beat one giant weekly sweep on both freshness and credit efficiency, since each curate session covers a single brand regardless of how many ads come back. If your team lives in no-code tools instead of crontabs, the same API calls drop into the recipes from our n8n, Zapier, and Make.com cookbooks, with an HTTP node replacing requests.

The step beyond scheduling is delegation. Because every response is clean JSON, these scripts double as tool calls for AI agents: hand the five functions to an agent and it can run the whole resolve-search-scan-enrich loop from a prompt. Two posts go deep on that pattern — Claude Code + adlibrary API workflows and building your own adlibrary MCP server, the latter in about 60 lines of Python.

What the Python Ad Library API Stack Costs

The Python side is free. The data side is credit-based, and the math is predictable enough to budget in a spreadsheet.

API access requires the Business plan at €329/mo, which includes 1000+ credits per month plus free integration help from the team. The unit costs from this cookbook: brand resolution is free, each search page is 1 credit, each curate session is 1 credit per brand, a winners scan is a flat 10, and enrichment is 1 credit per ad analyzed. Failed calls refund automatically.

Worked example for a ten-brand agency watchlist: nightly curate runs cost 10 credits per night (around 300 a month), a weekly winners scan of your three most important competitors costs 120 a month, and enriching 50 hand-picked winners adds 50. Total: roughly 470 credits, comfortably inside the Business allowance, with more than 500 left for ad-hoc keyword research. Stack that against the analyst hours the same loop costs manually, and against the return on ad spend impact of consistently rebuilding proven angles — the ROAS calculator makes that comparison concrete.

If you want to inspect the data before committing, the Starter plan (€29/mo, 50 credits) lets you run real searches in the app first. The key creation itself, though, is Business-only — see pricing for the current tiers.

Frequently Asked Questions

Do these Python scripts work with Meta's free Ad Library API?

No. They target the AdLibrary API, a paid multi-platform product. Meta's free Ad Library API remains useful for what it was built for, political and social-issue ad transparency on Meta platforms, but it doesn't return commercial ads in most regions, doesn't cover TikTok, YouTube, or LinkedIn, and requires app review plus a token refresh every 60 days.

How many credits does a nightly competitor-monitoring script use?

One curate session per brand per run. A ten-brand watchlist curated nightly costs 10 credits per night, about 300 per month. Pagination within each 30-minute session is free, and the Business plan's 1000+ monthly credits cover that with room for searches, winners scans, and enrichment on top.

What rate limits do I need to handle in Python?

Search allows 10 requests per minute per key, enrichment 20 per minute, and each key handles up to 10,000 requests per day. On a 429, read the Retry-After header and sleep that many seconds before retrying. Winners scans are additionally limited to one concurrent scan per account, so run them sequentially.

How do I avoid paying twice to enrich the same ad?

Three layers. Keep a local cache keyed on ad_key and skip the API entirely on a hit. Server-side, alreadyUnlocked: true means you previously paid for that exact ad and the call is free. A shared-cache hit (cached: true alone) still costs 1 credit, so the local cache is the layer that actually protects your budget.

Which Python libraries do I need for the AdLibrary API?

Just requests for HTTP and pandas for analysis. The API is plain REST with JSON responses and a single Bearer key, so there's no SDK to install. Anything that speaks HTTP works, and the same calls port directly to JavaScript, Go, or an n8n HTTP node.

Copy, Paste, Adapt

The point of a cookbook is that you don't cook from theory. These five Python ad library API scripts are deliberately boring: one shared client, flat functions, files on disk for state. Boring is what survives being scheduled, debugged at 7am, and handed to a teammate. Start with Script 1 and Script 2 today. Resolution is free, and your first searches will tell you immediately whether the data fits your ad creative workflow. Then wire Script 3 into cron and let the watchlist run for a week before you judge it.

When you're ready to run the loop for real, API access ships with the Business plan at €329/mo with 1000+ monthly credits, and the team helps with integration at no extra cost. The competitors you're tracking launched new creatives while you read this. The scripts are how you see them tomorrow morning.

Related Articles

Instagram ad campaign setup: three placements each with distinct creative layout
Guides & Tutorials,  Platforms & Tools

How to Build a Meta Ads Cron Job (Node + Python)

Build a production-ready Meta ads cron job in Node.js or Python. Schedule API calls, monitor spend, detect creative fatigue, and trigger Slack alerts automatically.