# LinkedIn Ad Library Scraper - Ads, Impressions & Targeting (`eiv/linkedin-ad-library-scraper`) Actor

Scrape the LinkedIn Ad Library by company, keyword or payer. Every ad a brand runs on LinkedIn, with impression ranges by country, the targeting the advertiser selected, and the agency that paid for it. No login and no API key.

- **URL**: https://apify.com/eiv/linkedin-ad-library-scraper.md
- **Developed by:** [Eimantas V](https://apify.com/eiv) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 ad scrapes

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, Impressions & Targeting

Every ad a company runs on LinkedIn, with the numbers LinkedIn is obliged to publish:
**impression ranges by country**, the **targeting the advertiser selected**, and the
**agency that paid**.

No login, no API key, no browser. Search by company, by keyword, or by payer.

### What you get for every ad

| Field | Notes |
|---|---|
| `advertiserName` | The brand whose page the ad runs under |
| `headline` | The ad's headline |
| `body` | The ad copy. LinkedIn shortens this to about 120 characters on a listing page — `bodyTruncated` tells you when you are holding the preview rather than the ad |
| `imageUrl` | The creative itself, or a video ad's cover frame |
| `adType`, `creativeType` | "Video Ad", `SPONSORED_VIDEO` |
| `url` | The ad's own page in the library |

With **Include impressions and targeting** switched on, each ad also carries:

| Field | Notes |
|---|---|
| `impressionsLabel` | LinkedIn's own range, e.g. `10k-20k` — it never publishes an exact number |
| `impressionsMin`, `impressionsMax` | The range parsed, so you can sort on it |
| `impressionsByCountry` | Share per country, e.g. `{"France": 100}` |
| `targeting` | The parameters the advertiser chose — language, location, audience, job, company |
| `payer` | Who actually paid — usually a media agency, and often the more interesting half |
| `destinationUrl` | The advertiser's own landing page — the one link in the record that leaves LinkedIn. Only some ad formats expose it; link-card ads usually do, organic-style sponsored posts usually do not, and a URL written into the ad copy stays in `body` rather than being reported here |
| `ctaText` | The button, e.g. "Learn more" |
| `firstShownAt`, `lastShownAt` | ISO 8601 |
| `body` | The full copy, typically around five times the listing preview |

Each target also gets a summary row: `totalAdsReported` is LinkedIn's own count for the
whole query, `adsFetched` is what this run collected, and `windowsSearched` is how many
date slices it took. The first two together tell you your coverage.

#### Use the residential proxy group

LinkedIn gates the ad library on the address a request comes from, and it refuses
datacenter addresses heavily and unpredictably — measured on one morning at 25%, then 6%,
then 100% within half an hour. This Actor retries on a fresh address automatically and
reports the refusal rate at the end of every run, but retrying cannot help when the whole
datacenter pool is being refused. The input defaults to **RESIDENTIAL** for that reason.

#### What LinkedIn does and does not publish

Measured across a real run of 15 Microsoft ads: all 15 returned the paying entity, and
**12 of 15 returned impressions, per-country shares and targeting.** LinkedIn does not
publish that analytics block for every ad, so those fields are sometimes null on a row
whose details were fetched successfully. You are not charged for a detail page that adds
nothing at all, and the run log says when that happens.

`headline` and `ctaText` belong to link-card ad formats. Organic-style sponsored posts —
`SPONSORED_STATUS_UPDATE`, the most common format — have neither, and the copy in `body`
is the whole ad. Null there means the format has no such field, not that it was missed.

### Why the payer field matters

Point this at a competitor and you learn more than their creative. A Microsoft ad comes
back paid for by **ZENITH UK (MEDIA) LIMITED** — so you learn which agency runs their
media, and you can then search by that payer to see every other brand it buys for.

### Pricing

Pay per result.

| Event | Price |
|---|---|
| Ad scraped | **$2.00 per 1,000** |
| Ad details fetched | **$2.00 per 1,000** — only when impressions and targeting are on |
| Actor start | $0.00005 |

Targets that cannot be reached are **never charged**, and an ad whose detail page fails is
still delivered without it — and not charged for the detail.

### Example input

```json
{
  "companies": ["https://www.linkedin.com/company/microsoft", "1441"],
  "includeDetails": true,
  "maxAdsPerTarget": 200,
  "countries": ["US", "GB"]
}
```

Companies accept a page URL, a numeric ID, or a name — but the difference matters more
than it looks. A **numeric ID** asks for one company. A **URL or a name** becomes
LinkedIn's own name search, which returns every advertiser whose name contains the text:
ask for `microsoft` and you also get "Devoteam | Microsoft Partner" and "ScriptRunner —
The #1 Microsoft Automation Platform". Both are real ads, but only one of them is
Microsoft's. The run log says which kind of match each target got, and the numeric ID is
the `companyIds` value in the URL when you filter to that company in LinkedIn's own ad
library.

### How it reaches more than 24 ads

LinkedIn's search returns **exactly 24 results** and has no offset parameter — `start`,
`from`, `count`, `page` and `offset` were each measured returning the identical 24, against
a Microsoft library LinkedIn itself reports as **11,318 ads**.

So this Actor pages by *narrowing the question*: it walks the date range in windows, and
any window that comes back full gets split in half and searched again, down to single days.
That is how it gets past 24, and it is why `maxAdsPerTarget` costs more requests than it
would on a normal paged API.

### Limits worth knowing

These are LinkedIn's, not this Actor's:

- **About one year of history.** LinkedIn's own date picker refuses earlier dates.
- **Impressions are ranges, never numbers.** `10k-20k` is what LinkedIn publishes.
- **A single day that still holds 24+ ads is the floor.** For the very largest advertisers a
  few ads on the busiest days are not reachable by any caller. The run says so when it
  happens rather than quietly returning less.
- **LinkedIn rate-limits by address.** The Actor detects it, reports it plainly, and stops
  rather than burning further addresses. Raise **Delay between requests** or use the
  **RESIDENTIAL** proxy group.

### Integrations

Results land in a standard Apify dataset — export to JSON, CSV or Excel, or push onward
with Make, Zapier, Google Sheets, Slack, or the API.

### Is this legal?

The LinkedIn Ad Library is a public transparency service, and this Actor reads only what
any logged-out visitor sees. It touches no private data and no account of yours. What you
do with the data remains your responsibility.

# Actor input Schema

## `companies` (type: `array`):

LinkedIn company page URLs, numeric company IDs, or company names. A numeric ID is the precise match; a name uses LinkedIn's own fuzzy search.

## `keywords` (type: `array`):

Free-text search across ad copy, for finding who advertises on a theme rather than a named brand.

## `payers` (type: `array`):

Search by who PAID for the ad rather than whose brand it carries - usually a media agency.

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

Two-letter country codes to restrict to. Leave empty for all.

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

YYYY-MM-DD. LinkedIn only keeps about one year of ads, so earlier dates return nothing.

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

YYYY-MM-DD. Defaults to today.

## `includeDetails` (type: `boolean`):

Fetch each ad's detail page for impression ranges by country, targeting parameters and the paying entity. One extra request per ad, charged separately.

## `maxAdsPerTarget` (type: `integer`):

LinkedIn serves 24 ads per query, so this Actor narrows the date range repeatedly to reach more. Raise it freely - the default is kept modest so a first run finishes quickly, not because the Actor is limited to it.

## `maxTotalAds` (type: `integer`):

A ceiling across every target in the run.

## `requestDelayMs` (type: `integer`):

LinkedIn rate-limits by address. Raise this if runs report a high refusal rate even on residential addresses.

## `proxyConfig` (type: `object`):

Use the RESIDENTIAL group. LinkedIn refuses requests by address, and it refuses datacenter addresses heavily and unpredictably: measured on 2026-09-22 at 25%, then 6%, then 100% within half an hour, the last of which collected nothing at all. This Actor retries on a fresh address automatically and reports the refusal rate at the end of every run, but no amount of retrying helps when the whole datacenter pool is being refused. If HTTP 407 appears in the log, the RESIDENTIAL group is not included in your Apify plan - switch the group back.

## Actor input object example

```json
{
  "companies": [
    "https://www.linkedin.com/company/microsoft"
  ],
  "keywords": [],
  "payers": [],
  "countries": [],
  "includeDetails": false,
  "maxAdsPerTarget": 50,
  "maxTotalAds": 2000,
  "requestDelayMs": 1500,
  "proxyConfig": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `ads` (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 = {
    "companies": [
        "https://www.linkedin.com/company/microsoft"
    ],
    "proxyConfig": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/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 = {
    "companies": ["https://www.linkedin.com/company/microsoft"],
    "proxyConfig": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("eiv/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 '{
  "companies": [
    "https://www.linkedin.com/company/microsoft"
  ],
  "proxyConfig": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call eiv/linkedin-ad-library-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eiv/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/NWd2Mb3t3bxSMvdeB/builds/o6fQoQGOlsmc74xdF/openapi.json
