DEV Community

Plain Signal
Plain Signal

Posted on

How to See Any Company's LinkedIn Ads with Python (Ad Text, Impressions and Targeting)

If you sell to businesses, your competitors are almost certainly advertising on LinkedIn: webinars, whitepapers, demo offers, "thought leader" posts from their executives. LinkedIn lets you see all of it. The LinkedIn Ad Library lists every ad that ran on LinkedIn in the past year, and for ads shown in the EU it even adds run dates, impressions, the country split and the targeting the advertiser chose (the EU's Digital Services Act requires it).

The catch: it's a search page for humans. No export, no API, 24 ads at a time, and a name search that's fuzzy. Search "HubSpot" and you also get "HubSpot Platinum Partner" and every agency with HubSpot in its name.

This tutorial shows how to get a company's LinkedIn ads as clean JSON or CSV with Python: a CSV of competitors' ads with landing pages and UTM campaigns, a check of which companies in a list run LinkedIn ads at all, and a Slack alert for new ads. It uses the LinkedIn Ad Library Scraper on Apify, which I built for this. All code is in the examples repo.

What you get per ad

A real record from a run on 2026-10-02 (HubSpot, shown in Germany; country list shortened):

{
  "advertiserName": "HubSpot",
  "advertiserId": "68529",
  "paidBy": "HubSpot, Inc.",
  "format": "IMAGE",
  "headline": "Kostenlos anmelden",
  "adText": "Pipeline-Problem erkannt, Kampagne gestartet: in Minuten. Live-Demo mit HubSpot am 30. Sept.",
  "callToAction": "Mehr erfahren",
  "landingUrl": "https://hubs.la/Q04vVMsQ0?utm_campaign=EMEA&utm_source=linkedin&utm_medium=paid&utm_id=805100113&...",
  "landingDomain": "hubs.la",
  "utm": {"campaign": "EMEA", "source": "linkedin", "medium": "paid", "id": "805100113"},
  "firstShown": "2026-09-01",
  "lastShown": "2026-09-29",
  "impressions": "100k-150k",
  "impressionsMin": 100000,
  "impressionsMax": 150000,
  "impressionsByCountry": [
    {"country": "Germany", "percent": 65, "label": "65%"},
    {"country": "Switzerland", "percent": 18, "label": "18%"},
    {"country": "Austria", "percent": 17, "label": "17%"}
  ],
  "targeting": {
    "Language": {"includes": ["Deutsch"], "excludes": []},
    "Location": {"includes": ["Deutschland", "Österreich und die Schweiz"], "excludes": []}
  },
  "targetedBy": ["Audience", "Company", "Job"],
  "adUrl": "https://www.linkedin.com/ad-library/detail/1515543043"
}
Enter fullscreen mode Exit fullscreen mode

Video ads also have an MP4 link (videoUrl), carousel ads list their cards, and document ads have the document title and page count. Ads a company runs through an employee's post ("thought leader ads") have the employee's name in postedBy.

Two things that make this harder than it looks

The name search is fuzzy. LinkedIn's "Search by company" returns anything that looks similar. The scraper takes a name, a website or a LinkedIn company URL, finds the advertiser whose name matches exactly (ignoring "Inc.", "GmbH" and the like; for monday.com it prefers the advertiser literally called "monday.com" over "monday group"), looks up its LinkedIn company ID and then filters the Ad Library by that ID. If nothing matches exactly, you get no ads and a log line with the similar names, not a look-alike company's ads.

LinkedIn blocks scrapers quickly. From a home connection, the Ad Library starts answering with HTTP 429 after about 15 quick requests, and it rejects cloud IPs outright. The scraper sends every request through a fresh proxy IP with a real browser's TLS fingerprint and retries on a new IP when one is blocked. A 300-ad run took two minutes.

Option 1: no code

  1. Open the LinkedIn Ad Library Scraper and click Try for free. A free Apify account is enough.
  2. Enter companies (HubSpot, hubspot.com or https://www.linkedin.com/company/hubspot) or keywords (webinar, crm software).
  3. Optionally pick a country and a date range.
  4. Click Start, then download the results as CSV, Excel or JSON.

Option 2: Python, competitors' ads as CSV

Install the Apify client and set your API token (Console → Settings → Integrations):

pip install apify-client
export APIFY_TOKEN=...
Enter fullscreen mode Exit fullscreen mode

This script collects the LinkedIn ads three CRM companies ran in the last 30 days and saves them with the call to action, landing domain and UTM campaign:

import csv
import os
from collections import Counter
from decimal import Decimal

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run = client.actor("plain-signal/linkedin-ad-library-scraper").call(
    run_input={
        "companies": ["HubSpot", "salesforce.com", "https://www.linkedin.com/company/pipedrive"],
        "dateRange": "last-30-days",
        "maxAdsPerSearch": 30,
    },
    max_total_charge_usd=Decimal("0.20"),  # 90 ads × $0.002
)
ads = list(client.dataset(run.default_dataset_id).iterate_items())

with open("competitor_linkedin_ads.csv", "w", newline="") as f:
    w = csv.writer(f)
    w.writerow(["advertiser", "format", "headline", "ad_text", "call_to_action", "landing_domain", "utm_campaign",
                "impressions", "first_shown", "last_shown", "ad_url"])
    for a in ads:
        w.writerow([a["advertiserName"], a["format"], a["headline"], a["adText"], a["callToAction"],
                    a["landingDomain"], a["utm"].get("campaign"), a["impressions"], a["firstShown"],
                    a["lastShown"], a["adUrl"]])

print(f"{len(ads)} ads saved to competitor_linkedin_ads.csv")
print("Formats:", ", ".join(f"{n} {f.lower()}" for f, n in Counter(a["format"] for a in ads).most_common()))
print("Top landing pages:")
for domain, n in Counter(a["landingDomain"] for a in ads if a["landingDomain"]).most_common(5):
    print(f"{n:>4}  {domain}")
Enter fullscreen mode Exit fullscreen mode

Output from a real run:

90 ads saved to competitor_linkedin_ads.csv
Formats: 50 image, 31 video, 7 carousel, 2 message
Top landing pages:
  23  pipedrive.com
  14  hubspot.com
   8  salesforce.com
   4  hubs.la
   1  au.linkedin.com
Enter fullscreen mode Exit fullscreen mode

The UTM campaign is often the most revealing column. One HubSpot ad in this run had utm_campaign=Marketing_Registrations_EN_EMEA_VARIOUS_GROW-Europe-2026_prospecting_..., which tells you the team, the goal, the region and the funnel stage before you even open the landing page. max_total_charge_usd is a hard cap on what the run can cost.

Option 3: which companies run LinkedIn ads?

For lead generation (agencies, B2B SaaS, martech sales), the question is often just "does this company spend money on LinkedIn?". Advertiser check mode returns one row per company:

import os
from decimal import Decimal

from apify_client import ApifyClient

COMPANIES = ["hubspot.com", "pipedrive.com", "close.com", "attio.com", "monday.com",
             "asana.com", "notion.so", "airtable.com", "zendesk.com", "intercom.com"]

client = ApifyClient(os.environ["APIFY_TOKEN"])

run = client.actor("plain-signal/linkedin-ad-library-scraper").call(
    run_input={"companies": COMPANIES, "mode": "advertisers"},
    max_total_charge_usd=Decimal("0.03"),  # 10 companies × $0.003
)
rows = list(client.dataset(run.default_dataset_id).iterate_items())
rows.sort(key=lambda r: r["adsLast30Days"], reverse=True)

for r in rows:
    count = f"{r['adsLast30Days']}{'+' if r['adsLast30DaysCapped'] else ''}"
    status = f"{count:>4} ads  {', '.join(f.lower() for f in r['formats'])}" if r["isAdvertising"] else "   no ads"
    print(f"{r['input']:<16} {status}")
print(f"\n{sum(r['isAdvertising'] for r in rows)} of {len(rows)} companies ran LinkedIn ads in the last 30 days")
Enter fullscreen mode Exit fullscreen mode
pipedrive.com    100+ ads  image, video
attio.com        100+ ads  document, image, message, video
hubspot.com      100+ ads  document, image, message, video
monday.com       100+ ads  carousel, document, image, video
asana.com        100+ ads  carousel, image, jobs, video
notion.so        100+ ads  document, image, video
airtable.com     100+ ads  document, follower, image, video
intercom.com     100+ ads  image
zendesk.com      100+ ads  article, document, image, message, video
close.com           no ads

9 of 10 companies ran LinkedIn ads in the last 30 days
Enter fullscreen mode Exit fullscreen mode

Counts stop at 100 per company to keep the check cheap. Add "country": "DE" to see who advertises in one market: in Germany, Pipedrive had 2 ads in the last 30 days and Asana 10.

Option 4: a Slack alert for competitors' new LinkedIn ads

Monitor mode remembers what earlier runs returned. Give it a name, and each run returns only ads it hasn't seen before:

import json
import os
import urllib.request
from decimal import Decimal

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run = client.actor("plain-signal/linkedin-ad-library-scraper").call(
    run_input={
        "companies": ["monday.com", "asana.com", "https://www.linkedin.com/company/notionhq"],
        "maxAdsPerSearch": 20,  # look at each competitor's 20 newest ads
        "monitorName": "pm-tools",  # remembers what earlier runs returned
    },
    max_total_charge_usd=Decimal("0.15"),
)
ads = list(client.dataset(run.default_dataset_id).iterate_items())

lines = []
for a in ads:
    what = a["headline"] or (a["adText"] or "").split("\n")[0][:120] or a["format"].lower() + " ad"
    lines.append(f"*{a['advertiserName']}* ({a['format'].lower()}): {what}\n{a['adUrl']}")
text = f"{len(ads)} new competitor LinkedIn ads\n\n" + "\n\n".join(lines) if ads else "No new competitor LinkedIn ads."

webhook = os.environ.get("SLACK_WEBHOOK_URL")
if webhook:
    req = urllib.request.Request(webhook, data=json.dumps({"text": text}).encode(),
                                 headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req)
else:
    print(text)
Enter fullscreen mode Exit fullscreen mode

A test run with one ad per company:

3 new competitor LinkedIn ads

*monday.com* (image): See what faster looks like on monday.
https://www.linkedin.com/ad-library/detail/1578871764

*Notion* (document): The exact decks, investor lists, and AI agents top startups used to close their first checks. Free guide, free Notion tr
https://www.linkedin.com/ad-library/detail/1619931196

*Asana* (image): Let AI Teammates coordinate campaign handoffs
https://www.linkedin.com/ad-library/detail/1563187794
Enter fullscreen mode Exit fullscreen mode

Run it again straight away and it prints No new competitor LinkedIn ads. Schedule it with cron or GitHub Actions, or skip the code entirely: create a schedule in the Apify Console and connect the Slack, Google Sheets or email integration.

What does it cost?

$0.002 per ad ($2 per 1,000) with all details, $0.001 per ad if you turn details off (text preview and image only), and $0.003 per company in advertiser-check mode. There's no monthly fee. The 90-ad CSV above costs $0.18, the 10-company check $0.03, and a weekly competitor monitor usually comes to cents a month. Apify's free plan includes monthly platform credit, which is enough to try all of the above.

Good to know

  • Run dates, impressions and targeting exist only for ads shown in the EU. For ads shown only in the US, for example, those fields are empty; the text, creative, advertiser, payer and landing page are always there.
  • Landing pages: lnkd.in short links in post text are resolved to the real destination, and the utm_* parameters are parsed into a dict.
  • Message ads keep LinkedIn's placeholders, such as Hi %FIRSTNAME%,.
  • Coverage: the Ad Library keeps ads for a year after they last ran. Newest ads come first.

Wrapping up

All three examples are in the plain-signal/examples repo. It also has examples for the Google Ads Transparency Scraper, which does the same for Google Search and YouTube ads. Together they show a competitor's whole paid funnel: what they bid on in search, and what they push to buyers' LinkedIn feeds. Questions and feature requests are welcome in the comments or on the actor's Issues tab.

This article was written with AI assistance. All code was run against the live scraper before publishing, and the outputs shown are real.

Top comments (0)