# LinkedIn Ad Library Scraper (`adatlas/linkedin-ad-library-scraper`) Actor

Search the LinkedIn Ad Library by keyword, company or payer. Get each ad's full copy, creatives, landing page and run dates - plus, for EU-served ads, total impressions and the complete country-by-country split that LinkedIn's own page hides behind a show-more.

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

## Pricing

from $1.20 / 1,000 scraped ads

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## LinkedIn Ad Library Scraper — competitor B2B ads, reach and targeting

Scrape the [LinkedIn Ad Library](https://www.linkedin.com/ad-library/home) by keyword, company
or payer — **no login, no LinkedIn API access, no cookies** — and export each ad's full copy,
creatives, landing page, run dates, and, for ads served in the EU, total impressions plus the
**complete country-by-country breakdown** and the advertiser's own **targeting** choices.
Download as JSON, CSV or Excel, or call it over the Apify API.

Ad-spy suites charge $100–500 a month and mostly cover Meta and TikTok. LinkedIn is where B2B
money goes, its library is thinner ground, and this bills per ad.

### What you can do with it

- **Competitor B2B teardown** — every ad a rival runs, with the full post copy and creatives
- **Reach intelligence no rival returns** — `impressions_by_country`, the complete split
  LinkedIn's own page hides behind a "show more"
- **Read their targeting** — which languages and locations the advertiser actually selected
- **Trace who is really behind an ad** — `paid_for_by` names the funding entity, usually the
  agency or parent company rather than the brand
- **Find the ads worth copying** — `minSpanDays` filters down to what an advertiser kept paying
  for rather than everything they tested
- **Agency prospecting** — payer search surfaces every brand a given agency buys for

### What you get

Every row carries:

- `advertiser_name` and `advertiser_url`
- **`paid_for_by`** — the legal entity behind the ad, which is often not the advertiser and is
  the fastest way to trace agencies and holding companies
- `format` — `single_image`, `video`, `carousel`, `article`, `message` or `text`
- **`ad_copy`** — the full post body, not the truncated preview
- `image_urls`, `video_urls`, `landing_url`
- `first_shown`, `last_shown`, **`span_days`** — set `minSpanDays: 30` and the output becomes
  the ads an advertiser kept paying for rather than everything they tested
- `impressions_total` as a parsed range (`{"min": 10000, "max": 20000, "text": "10k-20k"}`)
- **`impressions_by_country`** — every country with its share of impressions
- **`targeting`** — the advertiser's own targeting choices per facet (Language, Location, ...),
  with the complete value list: LinkedIn's page collapses it behind an "N others" button,
  this actor returns every entry

### Search an EU country to get reach data

This is the one thing worth knowing before your first run, and it is measured, not guessed:

| Search country | Total impressions | Country breakdown |
|---|---|---|
| EU — DE, FR, ES, IE, NL | essentially always | most ads, but it varies a lot by search |
| Non-EU — US, GB | only on ads that also served in the EU | same — a minority |

Measured, not guessed. **EU searches returned a total on 6/6, 12/12, 20/20 and 100/100 ads** in
separate runs. The country breakdown is the variable one: **98 of 100** ads on one
`adidas` / DE run, but 7/8, 5/8 and 4/8 on smaller samples for other keywords — so expect most
EU ads to carry it and some searches to carry it on nearly all.

The disclosure belongs to the **ad**, not to your search filter: an ad that ran in both the US
and the EU carries its EU reach data wherever you find it. On one US-only search, 2 of 10 ads
came back with the full split — one covering 30 countries — while the purely domestic ads
correctly reported `has_eu_disclosure: false`. You are only charged the `country-split` event
on the rows where the breakdown actually arrived.

LinkedIn publishes this to meet EU advertising-disclosure rules, so it tracks EU membership
rather than market size — the UK gets nothing for its own ads post-Brexit. Non-EU searches
always return copy, creatives, dates and the payer.

If you only want rows that carry the breakdown, set `onlyWithCountrySplit: true`.

#### Brand-new ads have a total but no split yet

LinkedIn publishes total impressions immediately but says on the ad's own page that
**impression data by country can take up to 48 hours**. So an ad that launched yesterday
comes back with `impressions_total` filled and `impressions_by_country` empty — and that row
carries **`country_split_pending: true`** so you can tell "not yet" from "never" instead of
wondering whether the scraper missed something. Search a broad keyword and you will hit a
run where most results are days old; add `minSpanDays: 3` to skip them entirely.

The `country-split` event is not charged on a pending row.

### The country breakdown is fuller than LinkedIn's own page

LinkedIn's ad detail page collapses the country list to four entries behind a "show more".
The underlying page carries the whole list — one sampled ad had **74 countries**. This actor
returns all of them.

### Input

```json
{
  "keywords": ["adidas"],
  "countries": ["DE"],
  "maxAdsPerQuery": 100,
  "minSpanDays": 0,
  "onlyWithCountrySplit": false
}
```

You can search three ways, and mix them: `keywords` (free text over copy and advertiser names),
`companies` (numeric LinkedIn company IDs, for one advertiser), and `payers` (the "Paid for by"
entity).

#### Every option

| Option | Default | What it does |
|---|---|---|
| `keywords` | — | Free text over ad copy and advertiser names. |
| `companies` | — | Numeric LinkedIn company IDs, e.g. `1035` for Microsoft. Use when you want one advertiser, not a keyword match. |
| `payers` | — | The legal entity on the "Paid for by" line — the way to trace an agency's whole book. |
| `countries` | `["DE"]` | Two-letter ISO codes. Country *names* are rejected. EU codes are the ones that carry reach data. |
| `maxAdsPerQuery` | `50` | Caps **each** search, so three targets at 50 is 150 ads and 150 charges. |
| `minSpanDays` | `0` | Keep only ads that ran at least this many days. |
| `onlyWithCountrySplit` | `false` | Keep only rows that actually carry the per-country breakdown. |
| `requestDelayMs` | `500` | Pace between requests. 500 ms is the measured safe pace; raise it if a run starts backing off. |
| `proxyConfiguration` | Apify Proxy on | Recommended — LinkedIn limits by IP, so rotation is what keeps a large run moving. Datacenter proxies are enough. |

Give none of the three target fields and the run falls back to a demo keyword rather than
failing, with a warning in the log.

### Speed, and why there is a delay setting

LinkedIn rate-limits by IP, and it is strict: six parallel requests trip a 429 immediately, and
once tripped the block lasts about **90 seconds** no matter how gently you retry. The actor
therefore works serially with a delay rather than in parallel.

At the default 500 ms it sustains roughly **50 ads per minute**. Leave the proxy enabled —
rotating IPs is what keeps a large run moving, and datacenter proxies are enough. If a run
starts backing off, raise `requestDelayMs`.

### Watch a competitor on a schedule

Ad monitoring is worth more as a diff than as a snapshot. Save the run as a task, give it a
schedule, and each run appends to the same dataset so you can see what launched and what was
retired:

1. Run it once with the companies or keywords you care about.
2. **Save as a new task**, then **Schedule** → daily or weekly.
3. Point the task's **Integrations** at Google Sheets, Slack, a webhook or your warehouse.

A daily 50-ad watch on one competitor costs about **$3.00 a month**.

### Use it from your own code

```bash
curl -X POST "https://api.apify.com/v2/acts/adatlas~linkedin-ad-library-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"keywords":["marketing automation"],"countries":["DE"],"maxAdsPerQuery":50}'
```

Official clients exist for Python and JavaScript, and the actor also works over MCP, so an AI
agent can call it directly.

### Pricing

Pay per event, so a search that finds nothing costs almost nothing:

| Event | Price | Charged |
|---|---|---|
| `apify-actor-start` | $0.0005 | once per run, by the platform, whatever the run returns |
| `ad-scraped` | $0.0012 | per ad delivered |
| `country-split` | $0.0008 | only when the country breakdown is actually present |

An EU run where most ads carry the breakdown lands near **$2.00 per 1,000 ads**; where fewer do,
it falls towards the **$1.20** base. A non-EU run, where no breakdown exists, is the base rate
only — you are never charged for a field that came back empty. Targeting rides along free.

### FAQ

**Do I need a LinkedIn account or the LinkedIn Marketing API?**
No. The Ad Library is a public archive; this actor uses no login, no cookies and no LinkedIn API
credentials, and accesses nothing private.

**Can I get impressions and country reach?**
Yes, wherever LinkedIn discloses it — which is ads served in the EU. The disclosure travels with
the ad, so an ad that also ran in the EU carries its reach data even on a US search.

**Can I search by company instead of keyword?**
Yes: `keywords`, `companies` (numeric LinkedIn company IDs) and `payers` (the "Paid for by"
entity) all work and can be mixed in one run.

**Can I export to CSV or Excel?**
Yes — every Apify dataset exports to JSON, CSV, Excel, XML and HTML table.

**Why is it slower than other scrapers?**
LinkedIn rate-limits hard by IP; going parallel trips a 429 and a ~90-second block. Running
serially is what keeps a large job finishing at all.

### Notes and limits

- Data comes from LinkedIn's public Ad Library, which LinkedIn publishes to meet advertising
  disclosure rules. No login is used and nothing private is accessed.
- `maxAdsPerQuery` caps each search. A keyword can match six figures of ads, so a run returns
  the cap, not the whole archive.
- Impression figures are ranges, because that is how LinkedIn publishes them. Both the parsed
  bounds and LinkedIn's original text are returned.
- Ads with no EU disclosure report `has_eu_disclosure: false` and null impressions rather than
  guessing.

# Actor input Schema

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

Free-text search over ad copy and advertiser names, one per line.

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

Numeric company IDs, e.g. 1035 for Microsoft. Use these when you want one advertiser rather than a keyword match.

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

The legal entity on the "Paid for by" line. Useful for tracing agencies and holding companies.

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

Two-letter codes such as DE, GB, FR. Leave empty for all countries. EU codes are the ones most likely to carry impression data. Must be ISO 3166-1 alpha-2 codes, not country names — "Germany" is rejected, "DE" is correct.

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

Each search can match tens of thousands of ads. This caps what one keyword contributes, and therefore what the run costs.

## `minSpanDays` (type: `integer`):

Keep only ads that ran at least this many days. Long-running ads are the ones an advertiser kept paying for.

## `onlyWithCountrySplit` (type: `boolean`):

LinkedIn publishes the per-country impression split for some ads but not all. Turn this on to keep only the ones that have it.

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

LinkedIn rate-limits aggressively per IP, and once tripped the block lasts about 90 seconds. 500 ms is the measured safe pace; raise it if you see the run backing off.

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

Recommended. LinkedIn limits by IP, so rotating proxies are what keep a large run moving. Datacenter proxies are enough.

## Actor input object example

```json
{
  "keywords": [
    "adidas"
  ],
  "countries": [
    "DE"
  ],
  "maxAdsPerQuery": 50,
  "minSpanDays": 0,
  "onlyWithCountrySplit": false,
  "requestDelayMs": 500,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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 = {
    "keywords": [
        "adidas"
    ],
    "countries": [
        "DE"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("adatlas/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 = {
    "keywords": ["adidas"],
    "countries": ["DE"],
    "proxyConfiguration": { "useApifyProxy": True },
}

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

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

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

```

## CLI example

```bash
echo '{
  "keywords": [
    "adidas"
  ],
  "countries": [
    "DE"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call adatlas/linkedin-ad-library-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=adatlas/linkedin-ad-library-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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