Integration Guide

How to Track Competitor Ads Across Facebook, Google, TikTok, and LinkedIn

The four official ad libraries don't cover the same ground. Here's how to pull all of them with one pattern, and the coverage gaps that make relying on just one a mistake.

Written by Alex P.

  • ad library
  • competitor ads
  • Facebook Ad Library
  • Google Ad Library
  • TikTok Ad Library
  • LinkedIn Ad Library

Ask how to track a competitor’s ads and most answers point at one place: open the Meta Ad Library, search the brand name, and scroll. That works for Facebook and Instagram. It tells you nothing about what the same competitor is running on Google, TikTok, or LinkedIn — and each of those has its own official transparency library, with its own rules about what it shows and to whom.

The result is what shows up in marketing subreddits over and over: someone builds a workflow around one library, then discovers months later that it never covered a whole platform, or that “no ads found” actually meant “no ads found in the countries this library covers.” The fix isn’t a smarter query. It’s knowing that these four libraries answer different questions, and building one pipeline that asks each the question it can actually answer.


The four libraries don’t match

Before writing any code, it’s worth being explicit about where they diverge, because every one of these gaps produces a false negative if you don’t know about it.

FacebookGoogleTikTokLinkedIn
Query matchesAd copy and creative textAdvertiser identity, not ad wordingAd copy (or advertiser name in advertiser_name mode)Ad copy, up to 500 characters
Country coverageAny country, ALL for every countryAny region, ANYWHERE for everywhere33 countries only — the 27 EU states plus GB, CH, NO, IS, LI, TR. US, CA, AU and everyone else is rejected outrightAny country
Political adsA dedicated adType filterNot filteredNone run at all — TikTok prohibits political advertising as a platform policyNot filtered
Spend / audience dataNot publishedNot publishedspend, estimatedAudience, uniqueUsersSeen — TikTok discloses more here than Facebook doesimpressions, and audience targeting categories
Impressions/targeting scopeN/AN/AN/AEU-targeted ads only — ads run outside the EU carry no impressions or targeting data at all

The practical read: if a competitor runs US-only TikTok ads, the TikTok Ad Library will never show them — not a data gap, a hard rejection at the country-code level. If you want to know how much a competitor spends on a specific LinkedIn ad, that number only exists for EU-targeted campaigns. And if you search Google’s ad library by brand name expecting to search ad copy, you’re searching the wrong field — query there matches advertiser identity, and domain is often the more reliable way in.

None of this is a defect in any one library. It’s four different disclosure regimes, shaped by different regulation in different markets, bolted onto four different products. Treating them as interchangeable is what produces the “ad library is clunky and limited” complaint — the limits are real, they’re just not uniform.


Pulling all four with one pattern

Each library takes a slightly different shape of query, so the pipeline is one function per platform, called with the same competitor name, merged into one list:

const API_KEY = process.env.FETCHLAYER_API_KEY;

async function fetchFacebookAds(brand) {
  const res = await fetch('https://api.fetchlayer.dev/facebook-ad-library/search-ads', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: brand, searchType: 'keyword_unordered', country: 'ALL', activeStatus: 'active', limit: 50 }),
  });
  return (await res.json()).ads ?? [];
}

async function fetchGoogleAds(brand, domain) {
  const res = await fetch('https://api.fetchlayer.dev/google-ad-library/search-ads', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    // domain is the more reliable match — query alone can resolve to the wrong
    // advertiser when several registered entities share a similar name.
    body: JSON.stringify({ domain, advertiserLimit: 3, region: 'ANYWHERE', limit: 50 }),
  });
  return (await res.json()).ads ?? [];
}

async function fetchTiktokAds(brand) {
  const res = await fetch('https://api.fetchlayer.dev/tiktok-ad-library/search-ads', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    // country: 'ALL' here means "every country TikTok's library covers" — not
    // every country. A brand that only advertises outside those 33 will come
    // back empty, correctly, with nothing in `notes` to flag it as unusual.
    body: JSON.stringify({ query: brand, queryMode: 'advertiser_name', country: 'ALL', adStatus: 'active', limit: 50 }),
  });
  return (await res.json()).ads ?? [];
}

async function fetchLinkedinAds(brand) {
  const res = await fetch('https://api.fetchlayer.dev/linkedin-ad-library/search-ads', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    // Widen dateRange before concluding a competitor has gone quiet — the
    // 30-day default window is easy to mistake for "no ads running."
    body: JSON.stringify({ query: brand, dateRange: 'last-year', country: 'ALL', limit: 50 }),
  });
  return (await res.json()).ads ?? [];
}

Normalizing into one competitor record

Every library names things differently — adArchiveId versus adId, a nested advertiser object shaped differently per platform — so the merge step is where you decide what “one ad” means across all four:

function normalize(source, ad) {
  return {
    source,
    id: ad.adArchiveId ?? ad.adId,
    url: ad.adLibraryUrl,
    isActive: ad.isActive ?? null,          // Google doesn't publish this; null, not false
    startDate: ad.startDate ?? ad.firstShownDate ?? null,
    spend: ad.spend ?? null,                // only ever populated by TikTok
    impressions: ad.impressions ?? null,    // only ever populated by LinkedIn, and only for EU-targeted ads
  };
}

async function watchCompetitor(brand, domain) {
  const [fb, google, tiktok, linkedin] = await Promise.all([
    fetchFacebookAds(brand),
    fetchGoogleAds(brand, domain),
    fetchTiktokAds(brand),
    fetchLinkedinAds(brand),
  ]);

  return [
    ...fb.map((ad) => normalize('facebook', ad)),
    ...google.map((ad) => normalize('google', ad)),
    ...tiktok.map((ad) => normalize('tiktok', ad)),
    ...linkedin.map((ad) => normalize('linkedin', ad)),
  ];
}

Resist the urge to force every field to exist on every row — spend genuinely doesn’t exist for a Facebook ad, and coercing it to 0 makes an unknown look identical to a real zero-spend campaign. null is the honest answer.


What to actually alert on

Once you’re pulling all four on a schedule, diff by id per source the same way you’d diff any other listing, and alert on new ads. The higher-value alert is a new platform: a competitor who has only ever run Facebook ads suddenly showing up in the TikTok library is a bigger signal than one more Facebook ad in a campaign that’s been running for months.

function detectNewPlatform(previousAds, currentAds) {
  const seenSources = new Set(previousAds.map((a) => a.source));
  const newSources = new Set(currentAds.map((a) => a.source));
  return [...newSources].filter((s) => !seenSources.has(s));
}

Practical notes

  • TikTok’s country list is a hard allowlist, not a suggestion. Sending US or CA as a country value is rejected outright, not silently ignored — check the response status before assuming a competitor has no TikTok ads.
  • Google’s query searches advertiser identity, not creative text. If a brand runs ads through an agency or a differently-named legal entity, searching the consumer-facing brand name can come back empty. domain is more reliable, and advertiserLimit above 1 catches brands registered under more than one name.
  • LinkedIn’s richest fields are EU-only. impressionsMin/impressionsMax and includedTargeting/excludedTargeting only ever populate for EU-targeted ads — a US-only LinkedIn campaign will come back with ad creative and dates but no spend signal, and that’s expected, not a partial failure.

Next Steps