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

Streamlit Marketing App: Ad Research in ~150 Lines

Build a Streamlit marketing app for competitor ad research in ~150 lines of Python: sidebar filters, cached API search, ad grid, watchlists, CSV export.

Streamlit marketing app for ad research: dashboard with sidebar filters and ad creative grid

Your team needs a competitor ad research tool. Engineering quoted six weeks. The off-the-shelf SaaS quote came back higher than your monthly test budget. There is a third path: a Streamlit marketing app you build yourself this afternoon, one Python file, roughly 150 lines, zero frontend skills required. If you can write a for loop and read JSON, you can ship a tool your whole team actually opens every morning.

TL;DR: Streamlit turns a Python script into a shareable web app, which makes it the fastest route from "I have an API key" to "my team has a tool." This guide builds a complete ad research app on the AdLibrary API: sidebar filters, a cached search call that protects your credit balance, an image-and-copy ad grid, watchlists held in session state, and one-click CSV export. Deploy it on Streamlit Community Cloud or inside your own network. About 150 annotated lines, start to finish.

Why a Streamlit Marketing App Beats Waiting on a Frontend Sprint

You may already pull ads programmatically. The Python ad library scripts cookbook covers fetch-filter-save patterns in detail, and they work. But a script only helps the person who runs it. The media buyer reviewing creative on a Tuesday morning does not want your terminal. She wants a search box, a country dropdown, and pictures.

The usual workarounds each hit a ceiling. A Google Sheets competitor ad dashboard is solid for scheduled reporting, but browsing video creatives in spreadsheet cells is miserable. n8n monitoring workflows excel at alerts and pipelines, yet nobody explores a market inside a workflow editor. And a proper web app traditionally means a React frontend, an API layer, auth, hosting. Six weeks, if the sprint holds.

Streamlit collapses that stack. You write a plain Python script. Every interactive element, a text input, a slider, a button, is a single function call that returns the user's current value. When someone changes a widget, Streamlit reruns your script top to bottom and re-renders the page. There is no state diffing to manage, no components to wire, no build step. The mental model is "my script runs again," and that is genuinely all of it.

The honest tradeoff: Streamlit is not built for ten thousand concurrent users or pixel-perfect brand experiences. It is built for the twelve people on your team who need ad intelligence in a browser instead of a notebook. That is exactly the job here, and a streamlit marketing app fills it better than anything else you can ship in a day.

What You're Building: Scope, Stack, and Credit Math

This streamlit marketing app does four things. It searches ads by keyword, platform, country, format, recency, and sort signal. It renders results as a visual grid with creative, copy, and performance numbers. It lets anyone pin interesting ads to a shared-session watchlist. And it exports everything to CSV for the slide deck someone inevitably asks for.

The stack is three pip installs: streamlit for the UI, requests for HTTP, pandas for the export. Nothing else.

The data source is the AdLibrary API. If the concept is new, start with what an ad library API is. The short version: Meta pioneered ad transparency with its free Ad Library API, and it remains the right choice if political and social-issue ads on Meta are all you need. Its constraints for commercial research are real, though: Meta-only coverage, app review before your first call, and full ad coverage limited to the EU and UK. Developers hit these walls constantly. The AdLibrary API is the paid power-user upgrade: every commercial ad across eleven platforms, Facebook, Instagram, TikTok, YouTube, Google, LinkedIn, Twitter, Pinterest and more, through one unified endpoint, with performance signals attached to every result and a single adl_ key instead of an OAuth dance.

Budget math before you write a line. Each search costs one credit, and a failed search refunds its credit automatically. API access ships with the Business plan at €329/month, which includes 1000+ monthly credits. The caching layer we build in this guide means a unique filter combination is paid once and then served free for fifteen minutes, so a five-person team running 25 to 30 distinct queries a day lands around 600 credits a month, comfortably inside the allowance. The app will display the remaining balance on every search so nobody has to guess.

Setup: Environment, Secrets, and Your First Request

Standard Python hygiene first, a virtual environment per the python.org venv docs:

bash
python -m venv .venv && source .venv/bin/activate
pip install streamlit requests pandas
mkdir -p .streamlit

Create your API key in the AdLibrary dashboard (Business plan). It starts with adl_, it is shown exactly once, and you can hold up to ten keys per account, which matters later for rotation. Put it in Streamlit's secrets file, never in code:

toml
# .streamlit/secrets.toml — never commit this file
ADLIBRARY_API_KEY = "adl_your_key_here"

Add .streamlit/secrets.toml to .gitignore right now, before the first commit exists. Inside the app, the key is available as st.secrets["ADLIBRARY_API_KEY"], per Streamlit's secrets management docs. Same mechanism works on Community Cloud later, so the code never changes between laptop and deployment.

Smoke-test the key before building anything:

bash
curl "https://adlibrary.com/api/search" \
  -H "Authorization: Bearer adl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"keyword": "running shoes", "appType": "3", "sortField": "-impression"}'

You should get JSON back: a results array of ads, a total count, and a _credits object with your remaining balance. That call cost one credit. Everything the app does is a dressed-up version of this request.

Here is the first chunk of app.py. The constants encode the API's documented vocabulary, and every sidebar widget maps to exactly one search parameter:

python
# app.py — a Streamlit ad research app on the AdLibrary API
import time

import pandas as pd
import requests
import streamlit as st

API_URL = "https://adlibrary.com/api/search"
PLATFORMS = ["facebook", "instagram", "tiktok", "youtube", "google",
             "linkedin", "twitter", "pinterest"]
MEDIA_TYPES = {"Image": "1", "Video": "2", "Carousel": "3", "Collection": "4"}
SORT_FIELDS = {
    "Most impressions": "-impression",
    "Hottest right now": "-heat_degree",
    "Longest running": "-days",
    "Newest first": "-first_seen",
}

st.set_page_config(page_title="Ad Research", layout="wide")

if "watchlist" not in st.session_state:
    st.session_state.watchlist = {}

with st.sidebar:
    st.header("Filters")
    keyword = st.text_input("Keyword or brand", value="protein powder")
    platforms = st.multiselect("Platforms", PLATFORMS,
                               default=["facebook", "instagram"])
    geo = st.text_input("Country code", value="US")
    media_label = st.selectbox("Format", list(MEDIA_TYPES), index=1)
    days_back = st.select_slider("Lookback window (days)",
                                 options=[7, 14, 30, 60, 90, 180], value=30)
    sort_label = st.selectbox("Sort by", list(SORT_FIELDS))
    run = st.button("Search ads", type="primary", use_container_width=True)

The mapping, widget by widget. keyword becomes the API's keyword parameter, the brand or ad-copy term to match. platforms becomes platform. Leave it empty to search everything. geo takes a country code, 50+ are supported. media_label translates to adsType, where 1 is image, 2 video, 3 carousel, 4 collection. days_back becomes daysBack, and the slider options are deliberately restricted to values the API accepts (1, 3, 7, 14, 30, 60, 90, 180, 365 are valid; the slider exposes the useful middle). sort_label becomes sortField, and these four sorts cover most research intents: -impression for reach, -heat_degree for the momentum score, -days for the long-runners that have proven themselves, -first_seen for fresh launches.

One parameter never appears in the sidebar: the app pins appType to "3", the e-commerce vertical, directly in the search call.

Note the run button. Streamlit reruns the script on every widget interaction, and without a gate the app would fire a paid search each time someone types a character into the keyword box. The button makes searching an explicit, deliberate act. That is your first cost control, and it costs one line.

The Search Call: Caching So Reruns Never Burn Credits

The rerun model is Streamlit's superpower and its billing hazard. Click anything and the script executes again from the top. If the API call sits unprotected in the script body, a teammate idly toggling the sort order burns a credit per click.

st.cache_data is the fix, and Streamlit's caching documentation is worth ten minutes of your time. Decorate the search function and Streamlit memoizes the result, keyed on the function's arguments. Same filters, same answer, zero API calls:

python
@st.cache_data(ttl=900, show_spinner="Searching ads...")
def search_ads(keyword, platforms, geo, media, days_back, sort_field):
    """One credit per unique filter combo, then free for 15 minutes."""
    payload = {
        "keyword": keyword,
        "appType": "3",              # 3 = e-commerce vertical
        "platform": list(platforms),
        "geo": geo,
        "adsType": media,
        "daysBack": days_back,
        "sortField": sort_field,
        "pageSize": 30,
    }
    headers = {
        "Authorization": f"Bearer {st.secrets['ADLIBRARY_API_KEY']}"
    }
    resp = requests.post(API_URL, headers=headers, json=payload, timeout=60)
    if resp.status_code == 429:
        wait = int(resp.headers.get("Retry-After", "30"))
        time.sleep(wait)
        resp = requests.post(API_URL, headers=headers, json=payload,
                             timeout=60)
    resp.raise_for_status()
    return resp.json()

Three deliberate choices here. The ttl=900 keeps results for fifteen minutes, long enough that five teammates comparing notes on the same query pay once, short enough that the data stays fresh for a market where creatives rotate daily. The platforms argument arrives as a tuple (you'll see the conversion in a moment) so the cache key stays stable. And the 429 branch honors the Retry-After header the API sends when you exceed its rate limit, sleeping once and retrying instead of hammering. The explicit timeout follows the requests library's own advice: production code should never wait forever.

Wire it to the button:

python
st.title("Competitor Ad Research")

if run and keyword.strip():
    st.session_state.results = search_ads(
        keyword.strip(), tuple(platforms), geo.strip().upper(),
        MEDIA_TYPES[media_label], days_back, SORT_FIELDS[sort_label],
    )

data = st.session_state.get("results")
if not data:
    st.info("Set your filters in the sidebar and hit Search.")
    st.stop()

credits = data.get("_credits", {})
st.caption(f"{data.get('total', 0):,} ads matched · "
           f"{credits.get('remaining', '?')} credits remaining")
if isinstance(credits.get("remaining"), int) and credits["remaining"] < 100:
    st.warning("Credit balance below 100 — searches may start failing soon.")

Storing the response in st.session_state.results means the grid survives subsequent reruns without re-querying, and the caption surfaces _credits.remaining from every response, so the balance is always visible to the whole team. The low-balance warning is two lines and saves an awkward Slack thread at month end.

Streamlit marketing app watchlist and CSV export view with saved ad cards

The Ad Grid: Creative, Copy, and Watchlists

Now the part your team will actually look at. Each ad in the response carries the creative URL, the copy, and the performance signals, so a three-column grid with metrics under every card takes surprisingly little code:

python
ads = data.get("results", [])
cols = st.columns(3)

for i, ad in enumerate(ads):
    with cols[i % 3]:
        if ad.get("preview_img_url"):
            st.image(ad["preview_img_url"], use_container_width=True)
        st.markdown(f"**{ad.get('advertiser_name', 'Unknown')}** · "
                    f"{ad.get('platform', '')}")
        copy_text = ad.get("body") or ad.get("title") or ""
        st.caption(copy_text[:180])
        if ad.get("button_text"):
            st.caption(f"CTA: {ad['button_text']}")

        m1, m2, m3 = st.columns(3)
        m1.metric("Days live", ad.get("days_count", 0))
        m2.metric("Heat", ad.get("heat", 0))
        m3.metric("Impr.", f"{ad.get('impression', 0):,}")

        key = ad["ad_key"]
        if key in st.session_state.watchlist:
            if st.button("✓ Watching — remove", key=f"rm-{key}"):
                del st.session_state.watchlist[key]
                st.rerun()
        else:
            if st.button("Watch this ad", key=f"add-{key}"):
                st.session_state.watchlist[key] = ad
                st.rerun()
        st.divider()

A quick read on the three metrics, because they decide what your team copies. days_count is runtime, and runtime is the strongest single signal in ad research: advertisers kill losers in week one, so a creative still live at day 60 is converting. The whole discipline of detecting winning ads programmatically builds on that logic. heat is a momentum score that catches what is scaling right now. And impressions show raw reach. Together they separate a quiet test from a committed winner at a glance, the judgment a creative strategist otherwise makes by scrolling for an hour.

The copy snippet under each image matters more than it looks. The first 180 characters usually contain the hook, and scanning thirty hooks side by side reveals the angles a market leans on faster than any report.

The watchlist runs on st.session_state, Streamlit's per-browser-tab dictionary that survives reruns. Click "Watch this ad" and the full ad object is stashed under its ad_key, the provider-prefixed identifier on every result. The two st.rerun() calls just refresh the button label immediately. This is a session-scoped swipe file: it lives as long as the tab does, which is exactly right for a research session, and the CSV export below is how anything worth keeping leaves the session.

CSV Export and Session State for the Rest of the Team

Half your team wants to browse; the other half wants the data in a spreadsheet by lunch. Both exports are a pandas DataFrame plus a st.download_button:

python
EXPORT_COLS = ["ad_key", "advertiser_name", "platform", "title", "body",
               "button_text", "impression", "heat", "days_count",
               "like_count", "share_count", "first_seen",
               "landing_page_url"]

def to_csv(rows):
    df = pd.DataFrame(rows)
    cols = [c for c in EXPORT_COLS if c in df.columns]
    return df[cols].to_csv(index=False).encode("utf-8")

left, right = st.columns(2)
with left:
    st.download_button(f"Download results CSV ({len(ads)} ads)",
                       to_csv(ads), file_name="ad-research.csv",
                       mime="text/csv", use_container_width=True)
with right:
    wl = list(st.session_state.watchlist.values())
    st.download_button(f"Download watchlist CSV ({len(wl)} ads)",
                       to_csv(wl), file_name="watchlist.csv",
                       mime="text/csv", use_container_width=True,
                       disabled=not wl)

The column allowlist keeps the export clean and stable even if the API adds fields. From here the data flows wherever your team already works: a competitor ad database with a real schema if you want history and queries, an Airtable swipe file if you want a visual archive, or straight into benchmarking, where the CPM calculator and ad spend estimator turn a competitor's reach numbers into "what would this cost us" answers.

That is the entire application. The file layout:

ad-research/
├── app.py                  # everything above, ~150 lines
└── .streamlit/
    └── secrets.toml        # your adl_ key, gitignored

Run it locally with streamlit run app.py and it opens at localhost:8501. That is a complete streamlit marketing app: about 150 lines, one afternoon, no frontend code anywhere.

Deploying Your Streamlit Marketing App: Community Cloud or Internal

A tool on localhost is a tool with one user, so deployment is where your streamlit marketing app earns the "team" part of its job description. Two routes cover almost everyone.

Streamlit Community Cloud is the fast one. Push the repo to GitHub (without the secrets file), connect it at share.streamlit.io, and paste your ADLIBRARY_API_KEY into the app's secrets panel in the dashboard. The st.secrets call in your code works unchanged. One caution that outranks all others: a Community Cloud app is reachable by anyone with the URL unless you restrict it, so use the viewer allowlist to limit access to your teammates' emails. An open URL wired to your API key means strangers can spend your credits. Restrict first, share second.

Internal hosting suits teams with stricter rules or existing infrastructure. The simplest version is a small VM inside your network running streamlit run app.py --server.port 8501 under systemd or in a Docker container, reachable only over the office network or VPN. You keep the secrets file on the server, your security team keeps its blood pressure, and the app needs no public exposure at all. For a 5-to-20-person marketing team, a 1-vCPU box is plenty, since the heavy lifting happens on the API side.

Pick by one question: does the key's blast radius worry you? If a leaked URL spending credits is an annoyance, Community Cloud with an allowlist is fine. If it is an incident report, host internally.

Hardening Notes: Keys, Rate Limits, and Cost Guards

Before you announce your streamlit marketing app in the team channel, four upgrades distinguish a weekend hack from a tool that survives the quarter.

Key hygiene. The key lives in st.secrets and nowhere else, never in code, never in the repo, never in a screenshot. Since the account allows up to ten keys, create separate ones per environment, streamlit-prod and streamlit-dev, so you can rotate one without touching the other. If a key ever leaks, revoke it in the dashboard and swap the secret. The app code never changes.

Rate limits. The API enforces a per-key rate limit on search. A cached single-page app sits far below it, but the moment someone adds a "scan all 20 competitors" loop, the 429s arrive. The Retry-After handling in search_ads covers the polite case. For genuine bulk work, schedule it outside the app, the way end-to-end monitoring pipelines do, and let the Streamlit app stay interactive.

Cost guards. Three are already built in: the button gate stops accidental searches, the 15-minute cache makes repeat queries free, and the credit caption keeps the balance visible with a warning under 100. A fourth is policy: failed searches refund their credit automatically, so a 500 never costs you anything, which means you can let the app fail loudly instead of wrapping everything in defensive retries.

Parameter discipline. The app exposes five filters, while the API accepts far more: engagement ranges, aspect ratios, CTA categories, e-commerce platform filters, explicit date windows. Resist adding them all at once. Each new widget multiplies unique cache keys, and every unique combination is a paid search. Add filters when a teammate asks twice. The full API documentation guide covers the complete parameter surface when you are ready.

From Internal Tool to Team Habit

The 150-line version is the seed, and its value compounds when the tool grows with the questions your team asks. Three natural next steps, in the order teams usually want them.

First, creative analysis. The API's enrichment endpoint takes any ad from your grid and returns a full creative teardown, transcript included, plus a 1:1 replication brief, for one credit. A "Generate brief" button on each watchlist card turns this app into the front end of a competitor-ad-to-creative-brief workflow. The pattern for running AI ad analysis at scale drops straight in, and the output is a structured creative brief your designers can execute against AI enrichment without ever opening an ad library by hand.

Second, scheduled freshness. The interactive app answers "what is running now," yet markets move overnight. Pair it with a nightly pull, GitHub Actions handles scheduled scans without a server, write results to a database, and point the Streamlit app at the stored data for instant, zero-credit browsing. Slack alerts on new competitor ads close the loop, and the broader pattern of automated competitor ad monitoring is a well-worn path.

Third, feed your testing pipeline. The watchlist plus the brief generator gives your creative testing program a steady supply of proven angles to adapt instead of cold guesses.

All of it runs on the same key. API access is part of the Business plan at €329/month with 1000+ monthly credits, sized for exactly this kind of always-on team usage. The details live on the API access feature page and pricing. If the build takes you longer than an afternoon, that plan also includes free integration help, so you are never debugging auth alone.

Frequently Asked Questions

How many lines of code does a Streamlit ad research app need?

About 150. The sidebar filters take roughly 25 lines, the cached search function 30, the ad grid with watchlist buttons 50, and the CSV export plus glue code the rest. It is one Python file with three dependencies (streamlit, requests, pandas), and every line is shown in this guide.

Do I need frontend skills to build a Streamlit marketing app?

No. Streamlit generates the entire web interface from Python function calls: st.text_input renders a text box, st.image renders an image, st.columns builds the grid. There is no HTML, CSS, JavaScript, or build tooling anywhere in the project, which is the core reason it is the fastest path from API key to team tool.

How do I stop a Streamlit app from wasting API credits?

Three guards. Gate the search behind an explicit button so Streamlit's rerun model never fires a paid call on a keystroke. Wrap the API call in st.cache_data with a TTL (15 minutes works well) so repeated identical queries are served from cache for free. And surface the _credits.remaining value from every response in the UI so the balance is always visible. Failed searches refund automatically.

Where should I deploy a Streamlit marketing app for a team?

Streamlit Community Cloud is fastest: push to GitHub, paste your API key into the secrets panel, and restrict access with the viewer allowlist so strangers cannot spend your credits. Teams with stricter security host internally instead, running the app on a small VM or Docker container behind the VPN with the secrets file on the server.

What does the AdLibrary API cost to run an app like this?

API access comes with the Business plan at €329/month, which includes 1000+ credits. Each search costs one credit and each AI creative analysis costs one credit, while advertiser lookups and the credit balance check are free. With the 15-minute cache, a five-person team running 25 to 30 unique queries a day uses roughly 600 credits a month.

Related Articles