# LinkedIn Ad Library Scraper: Ads by Company (`automation_craft/linkedin-ad-library-scraper`) Actor

Scrape the public LinkedIn Ad Library by company, keyword, payer or country with no login and no cookies. Get ad copy, headline, CTA, format, image and video URLs, the paying entity, run dates, impression ranges and EU targeting. JSON, CSV, Excel or API, schedulable, billed once per new ad.

- **URL**: https://apify.com/automation\_craft/linkedin-ad-library-scraper.md
- **Developed by:** [Automation Craft](https://apify.com/automation_craft) (community)
- **Categories:** Marketing, Lead generation, Social media
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 ads

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### LinkedIn Ad Library Scraper: Ads by Company

**Scrape LinkedIn ads** from the public LinkedIn Ad Library with no login, no cookies and no API key. Give it
a company, a keyword, a payer or a pasted Ad Library link and it returns every matching ad with the full ad
copy, the creative URLs, the paying entity and, for ads shown in the EU and the EEA, the run dates, the
impression range, the split by country and the targeting the advertiser chose.

It was built next to our Facebook, Meta and Instagram Ad Library scrapers, so it follows the same rules: you
pay once per new ad, duplicates are free, and the README tells you the measured fill rate of every field
instead of promising all of them.

### Quick start

1. Paste a LinkedIn company page URL or its numeric id into **Advertiser company pages or ids**. The prefill
   uses `1035`, which is Microsoft, capped at 8 ads so the first run is quick and cheap.
2. Optionally add **Keywords**, **Countries** and a **Date range**. Pick European countries if you want run
   dates and impressions (see the table below).
3. Set **Maximum ads in total** so a broad search cannot run away with your budget.
4. Click **Start**. Results appear in the **Ads** view; switch to **EU transparency** for the impression and
   targeting columns.
5. Export to JSON, CSV or Excel, or read the dataset from the API.

### What you get

One row per ad, plus one `run-summary` row per run with the counters. Fill rates below were measured on 160
ad detail pages sampled across six creative types worldwide, and on 24 ads from a German search.

| Field | What it is | Worldwide | Ads shown in the EU and EEA |
|---|---|---|---|
| `adId`, `url` | LinkedIn's ad id as a string, and the permanent ad page | 100% | 100% |
| `advertiserName`, `advertiserUrl`, `advertiserId` | The LinkedIn page that ran the ad | 100%, 96%, 96% | 100% |
| `paidBy` | The paying entity, often the media agency rather than the brand | 100% | 100% |
| `adFormat`, `creativeType` | Single Image Ad, Video Ad, Carousel Ad, Document Ad, Message Ad, Job Ad, Event Ad, LinkedIn Article Ad | 100% | 100% |
| `bodyText` | The full ad copy, not the truncated card teaser | 99% | 100% |
| `headline` | Headline under the creative | 86% | - |
| `ctaLabel` | Text on the ad's button, for example `Learn more` | 83% | - |
| `imageUrls`, `videoUrls`, `videoPosterUrls` | Creative media, one image entry per carousel slide, MP4 renditions for video ads | 92% have at least one | - |
| `firstShownDate`, `lastShownDate` | The dates the ad ran | 8% | 100% |
| `totalImpressions`, `totalImpressionsMin`, `totalImpressionsMax` | LinkedIn's impression band, for example `200k-300k` | 8% | 100% |
| `impressionsByCountry` | Share of impressions per country | 8% | 100% |
| `adTargeting` | Targeting parameters grouped by Language, Location, Audience, Company, Job and so on | 8% | 100% |
| `hasEuTransparency` | Tells you in one boolean whether the four rows above are filled | 100% | 100% |
| `alreadyKnown` | True when your dedup memory had already delivered this ad. Those rows are free | 100% | 100% |

The impression, run-date and targeting fields exist because of the EU Digital Services Act, so LinkedIn
publishes them for ads shown in the EU and the EEA and for almost nothing else. That is why the two columns
differ so much, and why every row carries `hasEuTransparency`.

### How much does it cost to scrape LinkedIn ads?

Pay per result, no monthly fee.

| Event | Price | When it is charged |
|---|---|---|
| **Ad** | **$0.001 each, so $1.00 per 1,000 ads** | Once for every new, unique ad delivered, ad details included |
| Actor start | $0.0005 per run, per gigabyte of memory | Once when a run starts |

Store volume discounts apply to the Ad event: Silver 10 percent off ($0.90 per 1,000), Gold, Platinum and
Diamond 20 percent off ($0.80 per 1,000). The Actor start event has no discount.

Nothing else is ever charged. Duplicates inside a run, ads your dedup memory already knows, ad ids that do
not exist, requests LinkedIn refused, status rows and the run summary are all free. The Actor runs on 256 MB
by default, so the start event is charged once.

Worked example: a weekly check on ten competitors that finds 300 ads the first week and 40 new ads a week
after that costs $0.3005 in week one and $0.0405 a week from then on, because the dedup memory makes the
other 260 free. A week where nobody launched anything new costs $0.0005.

### Input

| Input | What it does |
|---|---|
| `companyUrlsOrIds` | Company pages whose ads you want. A numeric URL or a bare id is an exact match; a vanity URL such as `/company/microsoft` has no id, so it is searched as an advertiser name and a warning says so |
| `searchKeywords` | Words or phrases in the ad text, one search per entry |
| `advertiserNames` | Advertiser names, when you do not have the numeric id |
| `payerNames` | The paying entity, for tracking one media agency across its clients |
| `adUrlsOrIds` | Look up specific ads directly |
| `searchUrls` | Paste any Ad Library search URL and its filters are read out of the link |
| `countries` | Two-letter country codes, empty for worldwide |
| `dateOption`, `startDate`, `endDate` | Last 30 days, this month, this year, last year or a custom range |
| `maxAds`, `maxAdsPerQuery` | Budget for the run and per search. The total is shared fairly between searches, then any leftover goes back to the searches that still have results |
| `includeAdDetails` | On by default. Off returns a faster creative list from the search cards only. The price per ad is the same either way |
| `dedupMemoryName` | Name a memory and later runs skip the ads it already delivered, free of charge |
| `emitKnownAds` | Return known ads too, flagged `alreadyKnown` and free, instead of skipping them |
| `resetDedupMemory` | Empty the memory first |
| `maxRequestsPerSecond`, `maxConcurrency` | Pacing. The defaults are the tested ones |
| `proxyConfiguration` | Leave the default residential setting |

Every search is independent, so five keywords and three companies run as eight searches with the same
country and date filters.

### FAQ

#### Do I need a LinkedIn account, cookies or an API key?

No. The LinkedIn Ad Library is a public, server-rendered page and this Actor reads it the way a signed-out
visitor does. Nothing in the input asks for a credential.

#### How far back does the LinkedIn Ad Library go?

Twelve months. The date picker on LinkedIn's own page is limited to the last year, so `current-year`,
`last-year` and a custom range all stop there. Ads older than that are not published by LinkedIn at all.

#### Why do some ads have impressions and run dates and others do not?

Those fields come from the EU Digital Services Act, so LinkedIn publishes them for ads shown in the EU and
the EEA. Measured, 24 of 24 German ads carried them and 13 of 160 ads from a worldwide sample did. Filter by
a European country, or read `hasEuTransparency` on each row.

#### Can I get the landing page URL the ad clicks through to?

No. LinkedIn does not publish the click destination on the Ad Library, so no scraper can read it from this
surface. The call to action label is there, the destination is not. LinkedIn also publishes no spend figures.

#### How do I stop paying twice for the same ad every week?

Put a name in `dedupMemoryName` and use the same name every run. The Actor keeps a private list of the ad
ids it has delivered under that name, skips them free of charge, and only charges for genuinely new ads.
Set `emitKnownAds` if you still want the known ads in the output, free.

#### Why does this Actor run with limited permissions?

Least privilege. It can only reach its own storages: the dataset it writes and the one named key-value store
it creates for your dedup memory. It cannot read anything else in your Apify account.

#### How many ads can one search return?

As many as LinkedIn has. The Actor pages through the site's own infinite-scroll endpoint 24 ads at a time
until LinkedIn says there are no more. A test query whose header claimed 664 matching ads returned all 664
unique ads. Use `maxAds` to cap large searches on purpose rather than by accident.

### What this Actor does NOT do

- **No landing page URLs and no ad spend.** LinkedIn does not publish either.
- **No exact impression counts**, only the band LinkedIn shows, such as `10k-20k`.
- **No impressions filter and no targeting filter.** Both exist on LinkedIn's own form and both are broken
  for signed-out visitors: the impressions filter returns a match count and zero rows, and the targeting
  filter silently widens the query instead of narrowing it. Shipping them would mean shipping wrong results.
- **No logged-in LinkedIn data.** No profiles, no connections, no Sales Navigator, no company employees.
- **No ads older than 12 months**, because the library does not hold them.
- Job ads carry no body copy, and their call to action can be an unresolved template such as
  `Careers at %COMPANYNAME%`. That is what LinkedIn serves; it is passed through rather than cleaned up.

### Using the API

Replace `<TOKEN>` with your Apify API token.

```bash
curl -X POST "https://api.apify.com/v2/acts/automation_craft~linkedin-ad-library-scraper/runs?token=<TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"companyUrlsOrIds":["1035"],"dateOption":"last-30-days","maxAds":50,"dedupMemoryName":"microsoft-weekly"}'
```

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<TOKEN>' });
const run = await client.actor('automation_craft/linkedin-ad-library-scraper').call({
    searchKeywords: ['cloud security'],
    countries: ['DE', 'FR'],
    maxAds: 200,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient
client = ApifyClient("<TOKEN>")
run = client.actor("automation_craft/linkedin-ad-library-scraper").call(run_input={
    "advertiserNames": ["Salesforce"],
    "dateOption": "current-year",
    "maxAds": 500,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item.get("headline"), item.get("totalImpressions"))
```

### Changelog

#### 0.1

First release: company, advertiser, keyword, payer, country and date search, direct ad lookup, full
pagination, ad details with EU transparency fields, and cross-run dedup memory.

### More data tools by Automation Craft

- [Facebook Ads Library Scraper - Page Ads, No Login](https://apify.com/automation_craft/facebook-ads-library-scraper)
- [Meta Ad Library Scraper - All Placements, Filters](https://apify.com/automation_craft/meta-ads-library-scraper)
- [Instagram Ads Library Scraper - Creatives, Video](https://apify.com/automation_craft/instagram-ads-library-scraper)
- [LinkedIn Jobs Scraper - No Login, Real Dates](https://apify.com/automation_craft/linkedin-jobs-scraper)
- [Google Trends Scraper - Compare and Trending Now](https://apify.com/automation_craft/google-trends-scraper)
- [Substack Scraper: Posts, Notes and Profiles](https://apify.com/automation_craft/substack-scraper)

# Changelog

This Actor's version history is a separate document: https://apify.com/automation\_craft/linkedin-ad-library-scraper/changelog.md

# Actor input Schema

## `companyUrlsOrIds` (type: `array`):

LinkedIn company pages whose ads you want, as URLs or numeric ids. A numeric URL such as https://www.linkedin.com/company/1035 or the bare id 1035 is an exact match. A vanity URL such as /company/microsoft carries no id, so it is searched as an advertiser name instead and a warning says so.

## `searchKeywords` (type: `array`):

Words or phrases to look for in the ad text, one search per entry. Example: cloud security.

## `advertiserNames` (type: `array`):

Advertiser or company names to search when you do not have the numeric company id. Example: Salesforce.

## `payerNames` (type: `array`):

The entity that paid for the ads, as printed on the ad detail page. Example: Essence Global LLC. Useful for tracking one media agency across its clients.

## `adUrlsOrIds` (type: `array`):

Look up specific ads directly. Accepts https://www.linkedin.com/ad-library/detail/1542548173 or the bare id 1542548173. An id that no longer exists returns a free status row.

## `searchUrls` (type: `array`):

Paste any LinkedIn Ad Library search URL. Its keyword, advertiser, company, payer, country and date filters are read out of the link.

## `countries` (type: `array`):

Two-letter country codes. Leave empty for worldwide. Ads shown in the EU and the EEA are the ones that carry run dates, impressions and targeting, so pick European countries when you want those fields.

## `dateOption` (type: `string`):

Which slice of the Ad Library to search. The library itself only holds the last 12 months, whatever you pick here.

## `startDate` (type: `string`):

First day of a custom range, as YYYY-MM-DD. Only used when the date range is set to Custom range.

## `endDate` (type: `string`):

Last day of a custom range, as YYYY-MM-DD. Only used when the date range is set to Custom range.

## `maxAds` (type: `integer`):

Hard cap on delivered ads across every search in this run. Leave empty for no cap. The budget is shared fairly between your searches, then any leftover is given back to the searches that still have results.

## `maxAdsPerQuery` (type: `integer`):

Cap per keyword, company, advertiser or payer. Leave empty to let one search use the whole budget.

## `includeAdDetails` (type: `boolean`):

Adds the full ad copy, the paying entity, the run dates, the impression range, the per-country split and the targeting parameters. Turn it off for a faster, lighter creative list. The price per ad is the same either way.

## `dedupMemoryName` (type: `string`):

Name a memory here and this Actor remembers every ad it has already delivered under that name. Later runs skip those ads free of charge, so a daily or weekly monitor pays once per ad. Use one name per project.

## `emitKnownAds` (type: `boolean`):

Returns ads the memory has seen before, flagged with alreadyKnown true and free of charge, instead of skipping them. Useful when you want the full current list every run.

## `resetDedupMemory` (type: `boolean`):

Empties the named memory first, so every ad found in this run counts as new and is charged again.

## `maxRequestsPerSecond` (type: `integer`):

How fast to ask LinkedIn. Eight is the tested default; higher values raise the share of refused requests, which the Actor then has to retry.

## `maxConcurrency` (type: `integer`):

How many ad detail pages to open at once.

## `proxyConfiguration` (type: `object`):

Leave the default. LinkedIn refuses a share of requests per exit IP, and residential exits are refused far less often than datacenter ones; the Actor recovers the rest by retrying on a fresh proxy session. With no proxy it cannot.

## Actor input object example

```json
{
  "companyUrlsOrIds": [
    "1035"
  ],
  "searchKeywords": [],
  "advertiserNames": [],
  "payerNames": [],
  "adUrlsOrIds": [],
  "searchUrls": [],
  "countries": [],
  "dateOption": "last-30-days",
  "maxAds": 8,
  "includeAdDetails": true,
  "emitKnownAds": false,
  "resetDedupMemory": false,
  "maxRequestsPerSecond": 8,
  "maxConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companyUrlsOrIds": [
        "1035"
    ],
    "searchKeywords": [],
    "advertiserNames": [],
    "payerNames": [],
    "adUrlsOrIds": [],
    "searchUrls": [],
    "countries": [],
    "dateOption": "last-30-days",
    "startDate": "",
    "endDate": "",
    "maxAds": 8,
    "includeAdDetails": true,
    "dedupMemoryName": "",
    "emitKnownAds": false,
    "resetDedupMemory": false,
    "maxRequestsPerSecond": 8,
    "maxConcurrency": 8,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation_craft/linkedin-ad-library-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companyUrlsOrIds": ["1035"],
    "searchKeywords": [],
    "advertiserNames": [],
    "payerNames": [],
    "adUrlsOrIds": [],
    "searchUrls": [],
    "countries": [],
    "dateOption": "last-30-days",
    "startDate": "",
    "endDate": "",
    "maxAds": 8,
    "includeAdDetails": True,
    "dedupMemoryName": "",
    "emitKnownAds": False,
    "resetDedupMemory": False,
    "maxRequestsPerSecond": 8,
    "maxConcurrency": 8,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("automation_craft/linkedin-ad-library-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companyUrlsOrIds": [
    "1035"
  ],
  "searchKeywords": [],
  "advertiserNames": [],
  "payerNames": [],
  "adUrlsOrIds": [],
  "searchUrls": [],
  "countries": [],
  "dateOption": "last-30-days",
  "startDate": "",
  "endDate": "",
  "maxAds": 8,
  "includeAdDetails": true,
  "dedupMemoryName": "",
  "emitKnownAds": false,
  "resetDedupMemory": false,
  "maxRequestsPerSecond": 8,
  "maxConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call automation_craft/linkedin-ad-library-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation_craft/linkedin-ad-library-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/lVehWd4fPNteXLPU5/builds/ukLCJNu0YioD3rfWV/openapi.json
