SocialCrawl

Ad library aggregation

Pull every ad a brand is running across Meta, Google, and LinkedIn in one parallel fan-out. 15 credits per brand audit.

You will build a competitive-intelligence view that shows every ad a target company is running across Meta, Google, and LinkedIn, flattened into one table you can render. Useful for sales-call prep, market-research reports, and buying-team intake.

Cost per run: 15 credits per brand audit (3 networks x 5 credits). Auditing a 25-company competitive set costs 375 credits.

How do you find all the ads a company is running?

Query the three public ad libraries (Meta, Google Ad Transparency, and LinkedIn) in parallel through one API key. Each network has a search endpoint that takes the brand name, or the domain for Google, and the three 5-credit calls complete in one Promise.all.

Every major ad network publishes a transparency library, but each lives behind a different UI, a different query model, and a different response shape, and none of the official UIs export data.

What you need

Three ad-library endpoints, all advanced tier (5 credits each):

EndpointNetworkParams
GET /v1/facebook/adlibrary/search/adsMeta Ad Libraryquery
GET /v1/google/company/adsGoogle Ad Transparencydomain or advertiser_id
GET /v1/linkedin/ads/searchLinkedIn Ad Librarykeyword and/or company, plus countries, startDate, endDate

The three networks do not share a parameter name. Meta takes query, Google takes domain (or advertiser_id), and LinkedIn takes keyword. Passing query to LinkedIn is silently ignored and returns an unfiltered page rather than an error.

Reddit's ad library is not part of this fan-out. Both Reddit ad endpoints, the reddit/ads/search keyword search and the reddit/ad single-ad lookup, are soft-disabled: the underlying data source returned consecutive 502s on 06/06/2026, so calling either now returns 503 at no charge. They stay registered pending a re-source. If you already have one of them wired up, drop the leg rather than retrying it, and treat Reddit paid coverage as a gap for now.

The code

The three responses have different upstream shapes, so the script flattens them into a unified { network, ad } row.

TypeScript
// recipe-ad-audit.ts
// Pulls every ad a brand is running across 3 networks in parallel.
// Run with: SOCIALCRAWL_KEY=sc_... npx tsx recipe-ad-audit.ts

const KEY = process.env.SOCIALCRAWL_KEY;
if (!KEY) throw new Error("Set SOCIALCRAWL_KEY");

const BASE = "https://www.socialcrawl.dev/v1";
const brand = "stripe";

async function get(path: string, params: Record<string, string>) {
  const url = new URL(`${BASE}/${path}`);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  const res = await fetch(url, { headers: { "x-api-key": KEY! } });
  if (!res.ok) return { success: false, error: await res.text() };
  return (await res.json()) as {
    success: boolean;
    data?: { items?: unknown[] };
    credits_remaining: number | null;
  };
}

type AdRow = { network: string; ad: unknown };

// Google ad-library searches by domain or advertiser_id, not free-text.
// Brand → domain is the most common shape; map your input accordingly.
const brandDomain = "stripe.com";

// Three networks, three parameter names. LinkedIn ignores `query`.
const [meta, google, linkedin] = await Promise.all([
  get("facebook/adlibrary/search/ads", { query: brand }),
  get("google/company/ads", { domain: brandDomain }),
  get("linkedin/ads/search", { keyword: brand }),
]);

const ads: AdRow[] = [
  ...(meta.data?.items ?? []).map((ad) => ({ network: "meta", ad })),
  ...(google.data?.items ?? []).map((ad) => ({ network: "google", ad })),
  ...(linkedin.data?.items ?? []).map((ad) => ({ network: "linkedin", ad })),
];

console.log(`${ads.length} ads found for "${brand}"`);
const counts = ads.reduce<Record<string, number>>(
  (acc, row) => ({ ...acc, [row.network]: (acc[row.network] ?? 0) + 1 }),
  {},
);
console.table(counts);

console.log(`credits left: ${linkedin.credits_remaining}`);

What you get back

JSON
// Final aggregated shape after the flattening loop:
[
  {
    "network": "meta",
    "ad": {
      "id": "ad_1234567890",
      "page_name": "Stripe",
      "ad_creative_body": "Accept payments online in minutes...",
      "first_active": "2026-05-01", // <-- normalised from raw upstream
      "active": true,
    },
  },
  {
    "network": "linkedin",
    "ad": {
      "id": "ln_987654321",
      "advertiser_name": "Stripe",
      "headline": "Built for fast-growing teams",
      "creative_url": "https://www.linkedin.com/ads/...",
    },
  },
  // ... google rows mixed in
]

What to change

  • brand and brandDomain: Meta and LinkedIn accept free-text company names; Google keys off the advertiser's domain, so keep both fields per company.
  • Widen the Meta leg: GET /v1/facebook/adlibrary/search/companies (5 credits) resolves a brand name to its advertiser pages, GET /v1/facebook/adlibrary/company/ads (5 credits) pulls one page's full run, and GET /v1/facebook/adlibrary/ad/transcript (10 credits) returns the spoken script of a video ad.
  • Widen the Google leg: GET /v1/google/adlibrary/advertisers/search (5 credits) turns a brand name into an advertiser_id, and GET /v1/google/ad (5 credits) pulls a single creative by URL.
  • Scope LinkedIn: add countries, startDate, and endDate to bound the run to a market and a campaign window.

On this page