adlibrary.com Logoadlibrary.com
Share
Competitive Research,  Guides & Tutorials

Notion Competitor Ad Hub: Build an API-Fed Database

Wire the AdLibrary search endpoint into Notion once, and your competitor ad hub fills itself nightly: deduped cards, gallery review views, and AI teardowns reserved for the winners.

Notion competitor ad hub: API-fed gallery database of competitor ad creatives

Your team already has a competitor ads page in Notion. Someone made it in a sprint of enthusiasm eight months ago, pasted in eleven screenshots, and nobody has touched it since. That page is where competitor intel goes to die. A Notion competitor ad hub fixes the underlying problem: the page was fed by hand, so it stopped being fed. Wire it to an ad library API instead and the database fills itself every morning, deduped, tagged, and ready for your Monday creative review.

TL;DR: Build four linked Notion databases (Ads, Brands, Hooks, Status views), connect a Notion integration token to a small sync script, and feed it from the AdLibrary search endpoint on a nightly schedule. Dedup on ad_key, render creatives in a gallery view, and route the best ads through an annotation workflow that ends in a creative brief. Cost: one credit per search, a Business plan (€329/mo), and about two hours of build time.

This guide walks the whole build: schema, integration script, scheduled sync with dedup, gallery views, team annotation, and the honest limits (video previews, rate caps) you should know before you start.

Why a Notion competitor ad hub beats the paste-dump wiki page

Notion is where competitor intel gets read. Strategy docs, campaign retros, client briefs, they all live there, and your team already has the tab open. That makes it the right destination for ad intelligence and the wrong place to do collection by hand.

Manual collection fails in a predictable sequence. Week one, the page is fresh and people comment on it. Week three, the person who owns it is busy with a launch and skips the update. Week six, a strategist references an ad that stopped running a month ago in a client deck. The wiki page didn't fail because Notion is bad at this. It failed because a static page has no inbound pipe.

A fed database inverts the maintenance burden. The script does the collecting, and humans do the only part humans are good at: judging which creative angle matters and what to do about it. Teams that run this pattern in Airtable swipe files or Google Sheets dashboards report the same shift. Review attendance goes up when there is always something new on the board.

There is also a data-quality argument. A screenshot has no metadata. An API row carries the advertiser name, platform, format, first-seen and last-seen dates, estimated spend, and a heat score. Your hub can sort by those signals. Your screenshot folder cannot. And because the AdLibrary API covers eleven platforms in one call (Facebook, Instagram, TikTok, YouTube, Google, LinkedIn, Twitter, Pinterest, Yahoo, Unity Ads, AdMob), the hub sees cross-platform activity your Meta-only checks miss.

One caveat worth naming early. Meta operates its own free Ad Library API, and if you only need political and social-issue ads on Meta surfaces, it costs nothing and you should use it. The AdLibrary API is the paid upgrade for commercial ads across every major network, with performance signals Meta's transparency tool does not expose. This build assumes you want the commercial picture.

Design the database schema: ads, brands, hooks, status

Resist the one-giant-table instinct. The hub works because each database answers one question, and relations stitch them together. Four databases cover it.

Ads is the workhorse. Every synced creative becomes one page with these properties:

PropertyTypeWhy it exists
NameTitleAdvertiser + first line of ad copy
Ad KeyRich textStable dedup ID from the API (ad_key)
BrandRelation → BrandsGroups ads by competitor
PlatformSelectfacebook, instagram, tiktok, youtube, linkedin...
FormatSelectimage, video, carousel, collection
First Seen / Last SeenDateRuntime math, fatigue spotting
Days RunningNumberThe strongest "this works" signal
Heat ScoreNumber0–1000 trending momentum from the API
Est. SpendNumberEstimated, never exact, still directional
HookRelation → HooksWhich opening pattern the ad uses
StatusStatusNew → Reviewed → Brief → Archived
CreativeFiles & mediaExternal URL to the ad image
Ad CopyRich textTitle, body, CTA text

Brands holds one page per competitor: domain, positioning notes, the platform IDs your sync script targets, and a rollup counting active ads. When an account manager asks "what is Brand X doing right now," this page is the answer.

Hooks is the database most teams skip and regret skipping. Each page is a recurring hook pattern: "founder talks to camera," "price anchor in first frame," "negative headline." Relating ads to hooks turns a pile of creatives into a pattern library, which is the actual point of competitive intelligence. After a month you can sort hooks by how many long-running ads use them.

Status is not a separate database, it is a status property plus saved views, but treat it as a first-class schema decision. Every ad arrives as New. A human moves it to Reviewed, promotes it to Brief if it deserves a build, or sends it to Archived. Without an explicit pipeline state, the hub becomes a feed nobody clears, which is the wiki-page failure mode wearing a database costume.

If you want a deeper relational design with SQL-style queries, the competitor ad database guide covers a Postgres version of this same schema. Notion trades query power for the place-your-team-already-reads advantage.

Set up the Notion side: integration, token, database IDs

Notion's API speaks plain REST, documented at developers.notion.com. Three setup steps, all in the UI:

  1. Create an internal integration at notion.so/profile/integrations. Name it ad-hub-sync, grant it read and insert content capabilities, and copy the secret token it issues.
  2. Share the databases with the integration. Open the Ads database, then Connections, then add ad-hub-sync. Repeat for Brands and Hooks. An integration sees nothing it has not been explicitly connected to.
  3. Copy the database IDs. The 32-character hex string in each database URL. Your script needs the Ads database ID at minimum.

Every API call carries two headers: Authorization: Bearer <secret> and a Notion-Version date string (pin 2022-06-28, the long-stable version). Creating an ad page is a single POST https://api.notion.com/v1/pages with the parent database ID and a properties object that mirrors your schema.

Two Notion constraints to design around now rather than discover later. Rich text fields cap at 2,000 characters per text element, so truncate long ad copy before writing it. And Notion enforces a rate limit averaging three requests per second per integration, returning 429 with a Retry-After header when you exceed it. A nightly sync writing 40 ads never feels this. A first backfill of 600 will, so throttle writes to roughly two per second and you stay clear.

Pull the ads: the AdLibrary side of the pipeline

The feed comes from the AdLibrary search endpoint. One key, one call, all platforms. You authenticate with an adl_ API key sent as a bearer token, created in your dashboard on the Business plan (no app review, no OAuth, no 60-day token expiry to babysit).

bash
curl "https://adlibrary.com/api/search" \
  -H "Authorization: Bearer adl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "keyword": "running shoes",
    "appType": "3",
    "platform": ["facebook", "instagram", "tiktok"],
    "daysBack": 7,
    "sortField": "-heat_degree"
  }'

The response is clean JSON: a total, a results array of ad objects, and a _credits block with your remaining balance. Each ad carries the fields your schema wants, including ad_key (the stable identifier you will dedup on), advertiser_name, message, preview_img_url, platform, first_seen, and last_seen as Unix timestamps.

For brand-specific tracking, two patterns work. Quick version: pass metaPageId with the competitor's Meta page ID to short-circuit the search to that one advertiser. Durable version: resolve the brand once with the free GET /api/advertisers/search lookup (it matches the brand across Meta, Google, and LinkedIn in one query), save it as an advertiser, then call the curate endpoint to pull every recent ad from all of its accounts, merged and deduped, for one credit per 30-minute session. The end-to-end monitoring guide compares both patterns in depth.

Each search costs one credit, and a failed search refunds automatically. Keep keyword pulls narrow (one niche keyword or one brand per call) rather than running a giant weekly sweep. Narrow and frequent beats broad and stale, and it keeps your estimated spend reads close to live.

Team reviewing synced competitor ads in a Notion competitor ad hub gallery view

The sync script, end to end

Here is the whole pipe in one Node.js file. It searches AdLibrary, checks Notion for each ad_key, and creates a page only for genuinely new ads. Node 18+ ships fetch globally, so there are no dependencies.

javascript
const ADL_KEY = process.env.ADLIBRARY_API_KEY;   // adl_...
const NOTION_KEY = process.env.NOTION_TOKEN;      // secret from your integration
const ADS_DB = process.env.NOTION_ADS_DB_ID;

const notionHeaders = {
  "Authorization": `Bearer ${NOTION_KEY}`,
  "Notion-Version": "2022-06-28",
  "Content-Type": "application/json",
};

// 1. Pull recent ads for one tracked keyword or brand
async function searchAds(keyword) {
  const res = await fetch("https://adlibrary.com/api/search", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${ADL_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      keyword,
      appType: "3",
      daysBack: 7,
      sortField: "-heat_degree",
    }),
  });
  if (res.status === 429) {
    const wait = Number(res.headers.get("retry-after") ?? 60);
    await new Promise(r => setTimeout(r, wait * 1000));
    return searchAds(keyword);
  }
  const data = await res.json();
  console.log(`${data.total} ads, ${data._credits.remaining} credits left`);
  return data.results;
}

// 2. Dedup: does this ad_key already exist in the Ads database?
async function adExists(adKey) {
  const res = await fetch(
    `https://api.notion.com/v1/databases/${ADS_DB}/query`,
    {
      method: "POST",
      headers: notionHeaders,
      body: JSON.stringify({
        filter: { property: "Ad Key", rich_text: { equals: adKey } },
        page_size: 1,
      }),
    }
  );
  const data = await res.json();
  return data.results.length > 0;
}

// 3. Create the Notion page for a new ad
async function createAdPage(ad) {
  const copy = (ad.message ?? "").slice(0, 1990); // 2000-char rich_text cap
  await fetch("https://api.notion.com/v1/pages", {
    method: "POST",
    headers: notionHeaders,
    body: JSON.stringify({
      parent: { database_id: ADS_DB },
      cover: ad.preview_img_url
        ? { type: "external", external: { url: ad.preview_img_url } }
        : undefined,
      properties: {
        "Name": { title: [{ text: {
          content: `${ad.advertiser_name}: ${copy.slice(0, 60)}` } }] },
        "Ad Key": { rich_text: [{ text: { content: ad.ad_key } }] },
        "Platform": { select: { name: ad.platform ?? "unknown" } },
        "First Seen": { date: {
          start: new Date(ad.first_seen * 1000).toISOString() } },
        "Ad Copy": { rich_text: [{ text: { content: copy } }] },
        "Status": { status: { name: "New" } },
      },
    }),
  });
}

// Glue: one tracked term per run keeps credit spend predictable
(async () => {
  const ads = await searchAds(process.argv[2] ?? "protein powder");
  for (const ad of ads) {
    if (await adExists(ad.ad_key)) continue;
    await createAdPage(ad);
    await new Promise(r => setTimeout(r, 400)); // stay under Notion's 3 rps
  }
})();

Forty lines of logic, and the hub feeds itself. The same structure ports to Python in twenty minutes if that is your team's language; the Python cookbook has equivalent request patterns, and the Node.js service guide shows how to grow this file into a proper wrapper with caching.

Set the Brand relation in a second pass if you track multiple competitors. Match advertiser_name against your Brands database once per run, cache the page IDs in memory, and pass the relation in properties as { "Brand": { relation: [{ id: brandPageId }] } }.

Schedule the sync and dedupe every run

A Notion competitor ad hub that updates when someone remembers to run a script is the wiki page again. Put the run on a clock. The cheapest scheduler you already have is a GitHub Actions cron workflow:

yaml
name: ad-hub-sync
on:
  schedule:
    - cron: "30 5 * * *"   # 05:30 UTC, before your team logs on
jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: node sync.js "your competitor keyword"
        env:
          ADLIBRARY_API_KEY: ${{ secrets.ADLIBRARY_API_KEY }}
          NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
          NOTION_ADS_DB_ID: ${{ secrets.NOTION_ADS_DB_ID }}

Prefer no-code? The same pipeline runs in n8n, Make, or Zapier with HTTP modules instead of a script. The architecture is identical: scheduled trigger, search call, dedup check, Notion create.

Dedup deserves its own paragraph because it is where naive builds rot. The ad_key field is the stable, platform-prefixed identifier AdLibrary returns for every creative, and it is the only thing you should trust for "have I seen this ad before." Do not dedup on image URL (CDNs rotate them) or on ad copy (variants share copy). The query-before-create pattern in the script costs one extra Notion read per ad and guarantees a clean board. For runs beyond a few hundred ads, batch it: pull all existing Ad Keys once into a Set, then check membership in memory.

One more scheduling habit: refresh Last Seen and Days Running on ads you already track. When a known ad_key reappears in results, update its page instead of skipping it entirely. Runtime is your best proxy for a winner, and it only stays accurate if the sync writes it back.

Tables are for sorting. Creative review needs to see the ads, and the right views turn a Notion competitor ad hub into a review room instead of a spreadsheet. Notion's gallery view renders each page as a card with its cover image, which is exactly why the sync script sets cover to the ad's preview_img_url.

Build three saved views on the Ads database:

  • Inbox. Gallery, filtered to Status = New, sorted by Heat Score descending. This is the Monday meeting view. New cards at the top, hottest first.
  • Winners wall. Gallery, filtered to Days Running > 30, grouped by Brand. Long-running ads are the ones competitors keep paying for, which makes this the densest signal on the board. Pair it with the winning creatives workflow when something here demands a response.
  • Hook board. Gallery, grouped by the Hook relation. Watching one hook pattern accumulate cards across three competitors is how you spot a market-wide shift weeks before it shows up in your own metrics.

Card previews come from the page cover, so image ads display beautifully. Video ads display their thumbnail only, a real limitation covered below. Keep card size medium and show three properties max (Brand, Days Running, Status) or the board turns into a spreadsheet wearing a trench coat.

The team annotation workflow: from raw card to creative brief

Data arriving is not intelligence. The annotation pass is where your team converts cards into decisions, and it works best as an explicit, lightweight ritual rather than an always-on expectation.

The cadence that holds up in practice: a 25-minute weekly review, owner rotates. The owner opens the Inbox view and triages every New card. Three outcomes per card, no fourth option:

  1. Archive. Not relevant, weak execution, or a duplicate concept. Most cards end here, and that is healthy.
  2. Review with notes. The strategist tags the Hook relation, writes one comment ("price anchor + UGC, third brand doing this in two weeks"), and sets Status = Reviewed. Notion comments keep the discussion attached to the ad itself instead of scattering it across Slack threads.
  3. Promote to Brief. The ad earns a build. Set Status = Brief, assign an owner with a Person property, and the card now appears in your creative pipeline view next to a written creative brief.

Two details make this stick. First, the empty Inbox is the goal. A triage that ends with twelve cards still marked New trains the team that the board is optional. Second, annotations belong on properties and comments, never in the ad copy field, so the synced data stays pristine and re-runnable.

If your team skews toward ad-spy browsing rather than scheduled review, run both. The hub does not replace a swipe file habit, it gives the habit a shared destination, the same way the swipe-file use case describes for manual saves.

AI teardowns inside the hub: enrich only the winners

Cards tell you what is running. They do not tell you why an ad converts. For the handful of ads your triage promotes, the AdLibrary enrichment endpoint runs a full creative teardown (timestamped transcript, hook analysis, persuasion levers, offer architecture) and returns a 1:1 replication brief as structured data:

javascript
const res = await fetch("https://adlibrary.com/api/enrichment", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${ADL_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    ad: {
      ad_key: ad.ad_key,
      platform: ad.platform,
      video_url: ad.video_url,
      message: ad.message,
    },
  }),
});
const { enrichment } = await res.json();
// append enrichment.summary as blocks on the ad's Notion page

Write the summary into the ad's page body and the strategist gets the teardown in the same place they review the card. Each analysis costs one credit (refunded on failure), so gate it behind the Brief status instead of enriching everything that syncs. Triage first, then spend. The AI enrichment guide covers prompt-level detail on what the analysis returns, and the AI enrichment feature page shows example output.

This is the loop the hub exists to close: spot the pattern in the gallery, read the teardown, brief the rebuild with your own product. Compare projected production cost against expected return with the ad budget planner before you greenlight the build.

Limits of a Notion competitor ad hub (read before you build)

A Notion competitor ad hub is the right tool for a strategist team, and it has edges. Know them up front.

No video previews in gallery cards. Notion covers render images. A video ad shows its thumbnail, and playing the actual creative means opening the source link. If your category is dominated by video, store video_url in a URL property and accept the click-through, or keep a Slack alert channel for instant creative checks alongside the hub.

Two rate limits, one pipe. Notion averages 3 requests per second per integration. AdLibrary allows 10 search requests per minute and 10,000 per day per key. Both return 429 with Retry-After, and the script above honors it. In practice the constraint only bites on backfills, so run your historical import once, slowly, overnight.

External image URLs can expire. The hub stores links, never copies of creatives. Platform CDNs eventually rotate ad media, so old archived cards may lose covers. For ads that matter long-term, download the creative and re-upload it to the page manually, or accept that the hub is a living board rather than a permanent archive. The Postgres database build is the better pattern for true archival.

Notion search is not a query engine. Rollups and filters cover the weekly workflow. The moment you want "average days running by hook pattern across brands, monthly," export to a real database. Build the hub for reading, not analytics.

Credits are a real budget line. Five tracked keywords, daily, is roughly 150 searches a month. Plus a few dozen enrichments, you are comfortably inside the Business plan's 1000+ monthly credits, but a careless loop that re-searches hourly will burn through them. Searches cost one credit each, period, so put the schedule on a leash and check the balance with the free /api/credits endpoint at the top of every run. Meta's API limitations cost you nothing in cash and plenty in coverage. This one is the reverse trade.

Frequently Asked Questions

Can I build a Notion competitor ad hub without writing code?

Yes. Replace the script with an n8n, Make, or Zapier scenario: a scheduled trigger, an HTTP request to the AdLibrary search endpoint, a Notion search step for dedup, and a Notion create-page step. The logic is identical to the Node.js version, and the no-code recipes in our Zapier and n8n guides map step for step.

How much does it cost to run a Notion competitor ad hub?

The Notion API is free on any paid Notion plan. The ad data side runs on the AdLibrary Business plan at €329/mo with 1000+ monthly credits, where one credit buys one search and one AI teardown costs one credit. A typical setup tracking five competitors daily uses 150-200 credits a month, leaving ample headroom for enrichment.

Why not use Meta's free Ad Library API to feed the hub?

Meta's free API is built for political and social-issue ad transparency. It requires app review, tokens that expire every 60 days, and outside the EU and UK it does not return general commercial ads at all. If political Meta ads are all you need, use it. For commercial ads across eleven platforms with spend estimates and heat scores, you need a paid ad intelligence API.

How do I stop duplicate ads from piling up in the database?

Dedup on the ad_key field, the stable platform-prefixed identifier returned with every ad. Before creating a page, query the Ads database for that key and skip or update on a match. Never dedup on image URLs or ad copy, since CDN links rotate and ad variants share text.

Can the hub show video ads inside Notion?

Partially. Gallery cards render the video's thumbnail image from the page cover, and the full creative stays a click away through a stored URL property. Notion does not autoplay or preview external ad videos on cards, so video-heavy teams usually pair the hub with the source links for playback.

Feed it once, read it forever

The difference between the wiki page that died and the hub that gets opened every Monday is one inbound pipe. Schema, token, forty lines of script, a cron line. None of it is hard, and all of it compounds: every week the Hooks database gets denser, the winners wall gets more honest, and your team's briefs start citing evidence instead of vibes.

Start with one competitor and one keyword. Prove the Monday ritual works before you scale the tracking list, and sanity-check what a rebuilt winner needs to return with the ROAS calculator before briefs leave the hub. When the ritual sticks, the Notion competitor ad hub pays for itself in the first angle your team catches early.

API access ships with the Business plan at €329/mo, including 1000+ monthly credits and free integration help, meaning our team will help you wire this exact sync into your workspace. Details and key setup live on the API access page. Your competitors' ads are already running. The only question is whether they land in a database your team reads.

Related Articles

Slack competitor ad alerts dashboard showing competitor ad notifications flowing into a Slack channel with creative thumbnails
Platforms & Tools,  Competitive Research

Slack Competitor Ad Alerts: A 30-Minute Build

Build Slack competitor ad alerts in 30 minutes: an incoming webhook, a Node poll script with first_seen filtering, dedup on ad_key, and cron scheduling.