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

Discord Competitor Ad Alerts: A Webhook Build

One webhook, a sixty-line poll script, and first-seen filtering: competitor ad alerts in the Discord server your DTC team already lives in.

Discord competitor ad alerts pipeline sending ad creative notifications into a chat channel

Your media buyer spotted the competitor's new ad three days after it launched. By then it was a badly cropped screenshot in a group chat, no link, no date, no context. Discord competitor ad alerts fix that failure mode with one webhook and a sixty-line script: every new ad a rival ships lands in your server as a rich embed, creative preview included, within hours of your poll catching it.

TL;DR: Create a Discord webhook in channel settings (two minutes, no bot, no OAuth). Poll the AdLibrary API on a schedule for each competitor, filter results by first_seen against a small state file so every ad alerts exactly once, then POST embeds with the creative thumbnail to your channel. Add a role mention only for genuine launches, split into per-competitor channels once volume grows, respect the rate limits on both ends, and run the whole thing from cron or GitHub Actions. Total build time: one evening.

Why Discord Competitor Ad Alerts Beat Another Browser Tab

Slack gets the corporate alert tutorials. We wrote one ourselves — the Slack version of this exact build walks through Block Kit formatting and Slack's workspace app model. If your team runs on Slack, start there.

Most lean DTC teams don't run on Slack. The founder, two media buyers, a freelance designer, and four UGC creators share a Discord server because Discord invites cost nothing and nobody has to provision a seat. The customer community often lives one server over. Ops conversations, creative feedback, launch coordination: all of it already happens in channels with custom emojis and a meme thread nobody will admit to reading.

That makes Discord the right delivery surface for competitive intelligence, for three practical reasons.

Webhooks are channel-native and free. Any server admin can mint a webhook URL in under two minutes. There is no app directory, no review queue, no workspace-level approval. Slack's incoming webhooks now require creating an app first. Discord's are a button in channel settings.

Embeds are built for visual content. An ad alert without the creative is a headline without the photo. Discord embeds render a full-width image, a color-coded side stripe, and up to 25 metadata fields by default. Your team sees the actual ad creative in the channel, not a link they have to click.

Everyone who needs the alert is already there. Alerts die when they land where people aren't. A notification channel inside the server your team already checks forty times a day has a response latency of minutes.

The build below assumes nothing beyond a Discord server you administer and an AdLibrary API key. No bot token, no gateway connection, no hosting beyond whatever runs your cron jobs.

The Pipeline: Four Parts, One Evening

Strip any competitor ad notification system down and you find the same four parts. Discord competitor ad alerts use the same skeleton as our end-to-end monitoring guide, specialized for one delivery surface.

  1. A data source that returns competitor ads as JSON.
  2. A poll script that runs on a schedule and fetches recent ads.
  3. A state store that remembers which ads you already announced.
  4. A webhook that turns new ads into channel messages.

The data source deserves a decision before you write code. Meta operates the original free Ad Library API, and credit where due: it invented programmatic ad transparency. It is also scoped for transparency rather than research. Outside the EU and UK it returns political and social-issue ads only, it covers Meta platforms exclusively, and access requires identity verification plus an app review, with tokens that expire every 60 days. The full list of constraints is its own article: seven walls developers hit.

This build uses the AdLibrary API instead — a paid upgrade, not a free alternative. One adl_ key returns commercial ads across Facebook, Instagram, TikTok, YouTube, Google, LinkedIn, Twitter, Pinterest and more from a single endpoint, with the fields an alert actually needs: first_seen, days_count, impressions, a 0–1000 heat score, and an estimated spend figure per ad. If a competitor quietly shifts budget to TikTok, a Meta-only pipeline never fires. A cross-platform pipeline does.

Wiring this kind of feed into a channel your team reads is the canonical competitor ad monitoring use case. Here is each part in order.

Step 1: Create the Discord Webhook

In your server, pick or create the alert channel (#competitor-ads is a fine start). Then:

  1. Open the channel settings gear → IntegrationsWebhooks.
  2. Click New Webhook, name it (Ad Alerts), optionally set an avatar.
  3. Copy Webhook URL.

Discord's own Intro to Webhooks covers the clicks with screenshots. The URL looks like https://discord.com/api/webhooks/{id}/{token}.

Treat that URL as a credential. Anyone holding it can post into your channel, so it goes in an environment variable or a secrets manager, never in committed code. If it leaks, delete the webhook and mint a new one — rotation takes thirty seconds.

Verify it works before writing any real code:

bash
curl -X POST "$DISCORD_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d '{"content": "Webhook live. Competitor ad alerts incoming."}'

A 204 No Content response means success. Append ?wait=true to the URL if you want the created message object back instead — useful later when you start threading. The full payload contract lives in the Execute Webhook docs.

Step 2: Wire Up the Ad Data Source

API access sits on AdLibrary's Business plan. Generate a key from your dashboard — it starts with adl_, you can hold up to 10 per account, and it is shown exactly once, so store it next to the webhook URL.

First, resolve each competitor to platform IDs. The advertiser lookup is free, so spend zero credits confirming you are tracking the right brand:

bash
curl -G "https://adlibrary.com/api/advertisers/search" \
  -H "Authorization: Bearer $ADLIBRARY_API_KEY" \
  --data-urlencode "q=Ridge Wallet" --data-urlencode "country=US"

The response includes a best_match with the brand's Meta page ID, Google advertiser ID, and LinkedIn company ID when at least two platforms agree it is the same advertiser.

From there you have two polling patterns:

  • Keyword search. POST /api/search with the brand name as keyword, appType: "3" (e-commerce), and sortField: "-first_seen" so the newest ads come back first. One credit per search, auto-refunded if the search fails.
  • Saved advertiser + curate. Save the brand once with its platform IDs, then POST /api/advertisers/{id}/curate to pull every recent ad it runs across all platforms, deduplicated, in one request. One credit per session. This is the better fit when one brand runs many accounts.

Keyword search is simpler and good enough for most alert pipelines, so the script below uses it. The credit math is friendly either way: five competitors polled twice daily is ten credits a day, around 300 a month — comfortably inside the Business plan's 1000+ monthly credits, with most of the allowance left for AI enrichment on the ads worth a closer look.

Step 3: The Poll Script with First-Seen Filtering

The whole system stands or falls on one question: has this ad been announced before? Get that wrong in one direction and the channel goes silent. Get it wrong in the other and your team mutes the channel by Thursday, which is the same as silence.

The answer is two filters layered together:

  1. A first_seen window. Only consider ads first observed in the last N days. This stops a year of back catalog flooding the channel on the first run.
  2. A seen-set. A JSON file of every ad_key already announced. ad_key is the stable identifier across calls, which makes it the right dedupe key — the same principle as the schema in our competitor ad database build.

Here is the complete script. Node 18+ ships fetch built in, so there are zero dependencies. Prefer Python? The same logic appears in our Python API cookbook, and the Node.js service version shows how to grow this into a real internal API.

javascript
// poll.js — Discord competitor ad alerts, zero dependencies (Node 18+)
import { readFileSync, writeFileSync, existsSync } from "node:fs";

const API_KEY = process.env.ADLIBRARY_API_KEY;
const WEBHOOK = process.env.DISCORD_WEBHOOK_URL;
const STATE_FILE = "./seen.json";
const WINDOW_DAYS = 7;

const COMPETITORS = ["Ridge Wallet", "Bellroy", "Ekster"];

const seen = new Set(
  existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, "utf8")) : []
);

async function searchAds(keyword) {
  const res = await fetch("https://adlibrary.com/api/search", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      keyword,
      appType: "3",
      sortField: "-first_seen",
      daysBack: WINDOW_DAYS,
    }),
  });
  if (res.status === 429) {
    const wait = Number(res.headers.get("retry-after") ?? 10);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return searchAds(keyword);
  }
  if (!res.ok) throw new Error(`search ${keyword}: HTTP ${res.status}`);
  return (await res.json()).results ?? [];
}

async function postAlert(ad, keyword) {
  const embed = {
    title: `${ad.advertiser_name} — new ${ad.platform} ad`,
    description: (ad.body || ad.title || "").slice(0, 400),
    url: ad.landing_page_url || undefined,
    color: ad.platform === "facebook" ? 0x1877f2 : 0x5865f2,
    image: ad.preview_img_url ? { url: ad.preview_img_url } : undefined,
    fields: [
      { name: "Platform", value: ad.platform, inline: true },
      {
        name: "First seen",
        value: new Date(ad.first_seen * 1000).toISOString().slice(0, 10),
        inline: true,
      },
      { name: "Running", value: `${ad.days_count ?? 0} days`, inline: true },
    ],
    footer: { text: `Tracked keyword: ${keyword}` },
    timestamp: new Date().toISOString(),
  };
  const res = await fetch(WEBHOOK, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ embeds: [embed] }),
  });
  if (res.status === 429) {
    const { retry_after } = await res.json();
    await new Promise((r) => setTimeout(r, retry_after * 1000));
    return postAlert(ad, keyword);
  }
}

for (const keyword of COMPETITORS) {
  const ads = await searchAds(keyword);
  for (const ad of ads) {
    if (seen.has(ad.ad_key)) continue;
    seen.add(ad.ad_key);
    await postAlert(ad, keyword);
    await new Promise((r) => setTimeout(r, 1500)); // stay polite on both APIs
  }
}

writeFileSync(STATE_FILE, JSON.stringify([...seen]));
console.log(`Done. ${seen.size} ads tracked.`);

One deliberate choice: the script marks an ad as seen before the webhook call rather than after. If Discord hiccups you lose one alert, which beats the alternative — a crash loop that re-announces the same ad forever. Swap the order if you prefer at-least-once delivery.

On the very first run, every ad inside the seven-day window posts at once. Either run it once with the webhook line commented out to warm the state file, or accept one noisy minute as a christening.

Discord embed formatting for competitor ad alerts with creative preview thumbnail

Step 4: Embed Formatting with Creative Previews

The default embed in the script works. A tuned embed gets read. Discord gives you a generous canvas, and the webhook payload spec sets the boundaries: up to 10 embeds per message, 6,000 characters across all of them combined, a 256-character title, a 4,096-character description, and up to 25 fields of 256/1,024 characters each.

A few formatting decisions carry most of the weight:

Use image, not thumbnail, for the creative. The thumbnail slot renders as a small square in the corner. The image slot renders full-width. Your team's eye should land on the competitor's creative first and the metadata second, because the hook is the thing worth stealing.

Color-code the side stripe by platform. The color field takes a decimal integer. Meta blue (0x1877F2) for Facebook and Instagram, black for TikTok, red for YouTube. After a week, your team reads platform from the stripe without reading a single field.

Put the verdict signals in inline fields. Three inline fields sit on one row. The trio that matters for a fast read: how long the ad has been running, the impression signal, and the estimated spend. Runtime is the strongest single tell — losers get killed in week one, so anything past day 30 has earned its budget. When the estimated spend field makes you suspicious a rival is scaling, sanity-check the number against your own market assumptions with the ad spend estimator.

Link the landing page in url. Setting url makes the embed title clickable. The landing page tells you the offer and funnel stage faster than the creative does.

One honest caveat about previews: preview_img_url points at a creative snapshot, and ad CDN URLs can expire. If an embed renders without its image a week later, that is the source URL aging out, not your pipeline breaking. For Discord competitor ad alerts it rarely matters because the alert is read within hours — but archive creatives you want to keep into something permanent, the way the Airtable swipe file build does.

For ads that clear your "worth a closer look" bar, a follow-up call to the enrichment endpoint returns a full creative teardown for one credit: transcript, persuasion analysis, and a 1:1 replication brief. Post it into the same channel as a reply and you have gone from alert to actionable brief without leaving Discord.

Role Mentions for Launch Moments

A channel that pings for every ad trains everyone to ignore it. The fix is a severity tier: silent embeds for routine ads, a role mention for launches.

Create a @launch-watch role in server settings and let people opt in. Then mention it from the webhook payload only when an alert clears a launch heuristic:

javascript
const isLaunch = newAdsToday >= 5; // 5+ new ads from one brand in one day
const payload = {
  content: isLaunch ? `<@&${process.env.LAUNCH_ROLE_ID}> possible launch` : "",
  embeds: [embed],
  allowed_mentions: { parse: [], roles: [process.env.LAUNCH_ROLE_ID] },
};

Two details matter here. The mention syntax is <@&ROLE_ID> with the role's numeric ID, not its name — grab the ID by right-clicking the role with developer mode on. And allowed_mentions is your safety rail: with parse set to an empty array and the role whitelisted explicitly, a competitor ad whose copy happens to contain @everyone can never ping your whole server.

What counts as a launch? Volume spikes are the simplest signal — five or more new creatives from one advertiser in a day means a campaign push, since brands batch-test creative variants at launch. A first-ever ad on a new platform is a second strong signal. Our guide on detecting competitor campaign launches builds the full heuristic stack, including how creative testing patterns give launches away early. And before anyone panic-spins a counter-campaign at 9pm, run the numbers through the break-even ROAS calculator. Reacting to a rival's launch is a budget decision, not a reflex.

Channel Architecture: Per-Competitor vs Digest

Where alerts land shapes whether they get used. Two architectures work, and the right one depends on volume.

The digest channel. One #competitor-ads channel receives everything. Right for one-to-four competitors with modest ad velocity. Everything sits in one scrollable timeline, and cross-brand patterns jump out — three rivals all shipping UGC mashups in the same week is a trend you only notice when their ads interleave.

Per-competitor channels. A COMPETITORS category containing #ads-ridge, #ads-bellroy, #ads-ekster, each with its own webhook URL. Right from roughly five competitors up, or whenever a single brand's volume drowns out the rest. Each channel becomes a clean chronological dossier of one rival — scroll #ads-ridge top to bottom and you are reading their creative strategy in order. This is the channel a media buyer opens before a planning call, the in-Discord equivalent of an ad spy workspace.

The hybrid most teams settle on: per-competitor channels for the full firehose, plus one digest channel that only receives launch-tier alerts. Routine ads file themselves into dossiers. The mention-worthy events surface in the room everyone reads.

Implementation is one map in the script, WEBHOOKS = { "Ridge Wallet": url1, "Bellroy": url2 }, because webhooks are channel-scoped. Mute the firehose channels by default and let the role mention cut through. Discussion happens in threads off the alert message, which keeps the alert channel itself a clean timeline of competitor ad alerts in Discord.

Rate Limits on Both Sides of the Pipe

Two services, two ceilings, both documented and both generous for this workload.

Discord. Webhook deliveries are capped at roughly 30 messages per minute per channel, with the standard rate-limit headers on every response. Exceed the cap and you get a 429 whose JSON body includes retry_after in seconds — the script above already honors it. If a burst of new ads exceeds the cap, you can also batch: a single webhook call carries up to 10 embeds, so 30 requests a minute is genuinely 300 ads a minute of capacity. No alert pipeline gets near that.

AdLibrary. The API allows 10 requests per minute and 10,000 per day per key, and 429 responses carry a Retry-After header so a client can back off cleanly. Ten competitors polled sequentially with a one-second gap fits inside a single minute's budget. Credits are the other meter: one per search, one per enrichment, with failed searches refunded automatically — a 500 never costs you anything.

The 1500ms sleep between webhook posts in the script keeps you comfortably under both ceilings simultaneously. Boring, and correct.

Cron Options: Running Discord Competitor Ad Alerts on a Schedule

The script is stateless apart from seen.json, so anything that can run Node on a timer can run it.

A crontab on any VPS. The classic. Twice daily at 7:00 and 15:00:

cron
0 7,15 * * * cd /opt/ad-alerts && /usr/bin/node poll.js >> poll.log 2>&1

GitHub Actions. No server at all. A schedule trigger runs the script in a fresh runner, with the key and webhook URL as repository secrets and seen.json committed back to the repo as state. The complete pattern, including the state-commit step and the fact that scheduled runs can drift a few minutes, is in our GitHub Actions monitoring build.

yaml
on:
  schedule:
    - cron: "0 7,15 * * *"

n8n. If you prefer visual workflows or want non-developers to own the pipeline, an n8n Schedule node feeding an HTTP Request node replicates this build without code. Six variants live in the n8n monitoring cookbook, and the Zapier recipes cover the no-code equivalent.

A long-running process. node-cron or a plain setInterval inside a process you already host. Only worth it if the process exists anyway — a dedicated always-on box for a twice-daily poll is overkill.

On frequency: resist hourly. Ad libraries index new ads on a lag measured in hours, so polling more often than every few hours mostly re-reads the same data while spending credits. Twice daily catches a launch the day it happens, which is the decision speed that matters — you are deciding whether to respond this week, not this minute. Twice daily across five competitors is ~300 credits a month. Hourly across ten is ~7,200, which is the difference between fitting a plan and funding one.

Frequently Asked Questions

Do I need a Discord bot to send competitor ad alerts?

No. A webhook covers this entire build — posting messages, embeds with images, and role mentions into one channel, with no bot token, no gateway connection, and no hosted process listening for events. You only need a real bot when you want interactivity, like slash commands or buttons that trigger follow-up lookups.

How much does running Discord competitor ad alerts cost?

Discord webhooks are free. The data side runs on AdLibrary's Business plan at €329/mo, which includes API access and 1000+ monthly credits. Each poll costs one credit per competitor searched, so five competitors polled twice daily uses about 300 credits a month, leaving most of the allowance for AI creative analysis on the ads that matter.

Can I get alerts for TikTok and YouTube ads, not only Facebook?

Yes. The same /api/search call covers Facebook, Instagram, TikTok, YouTube, Google, LinkedIn, Twitter, Pinterest and more, and each result carries a platform field you can route on — or filter with the platform parameter. Meta's free Ad Library API, by contrast, covers Meta platforms only and limits most regions to political and social-issue ads.

How do I stop duplicate alerts for the same ad?

Dedupe on ad_key, the stable identifier each ad keeps across API calls. Persist every announced key in a state file or database, skip anything already present, and pair it with a first_seen window (such as the last 7 days) so your first run doesn't replay a competitor's entire back catalog into the channel.

How fast will I know when a competitor launches a new ad?

Polling twice daily, you will typically see a new ad within 12 to 36 hours of it going live, since ad libraries index new creatives on a lag of hours. That is well ahead of the industry norm of noticing a campaign weeks in, and fast enough for the decision that actually matters: whether to respond with your own creative this week.

Ship It Before Friday's Standup

The gap between "we should watch competitors more closely" and a working system is one evening of glue code. Discord competitor ad alerts are the rare automation where the hard parts are already built: Discord supplies the delivery surface and the formatting, the ad library supplies structured cross-platform data with first_seen timestamps, and your sixty lines in the middle just decide what is new and what deserves a ping.

Build the minimum first — one webhook, one channel, three competitors, twice-daily cron. Tune the launch heuristic after you have a week of real volume, split channels when the digest gets noisy, and add the enrichment follow-up once the team starts asking "why is this ad working?" From there the same feed can drive an AI agent that drafts the competitive summary before you have read the alert.

API access ships with the Business plan at €329/mo — 1000+ monthly credits, every platform through one key, and integration help from the team when you wire it into your stack. The API access feature page has the endpoint overview. Your competitors' next launch is already in production. The only question is whether your server hears about it.

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.