# LinkedIn Ad Library Scraper - Reliable (`atlasdata/linkedin-ad-library-scraper-reliable`) Actor

Scrape the LinkedIn Ad Library by advertiser, keyword, payer, country and date. Get ad text, creatives, CTA, landing URL, run dates, impressions, country split and targeting. Real Chrome + residential proxy, per-input status, $1.50 per 1,000 ads with details.

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

## Pricing

from $0.80 / 1,000 ad scrapeds

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/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 - Reliable

Scrape the public [LinkedIn Ad Library](https://www.linkedin.com/ad-library) by **advertiser / company name, keyword, "paid for by" payer, country and date range**. You get every ad's text, creative (images, video file, carousel), headline, CTA, landing URL, and for ads shown in the EU the transparency data: **run dates, impression ranges, impression share by country and targeting** (languages, locations, audience, company, job and demographic parameters).

It runs in a real Chrome browser on residential IPs, because LinkedIn's Cloudflare protection blocks plain HTTP scrapers (about 90% of plain requests got HTTP 403 in our tests). Blocked or rate-limited sessions are swapped for new ones and the run picks up where it stopped, so a 403 does not lose results.

### What can you use it for?

- **Competitor ad research**: see every ad a competitor is running on LinkedIn, with copy, creative and landing pages.
- **Messaging and creative benchmarks**: compare formats (image, video, document, carousel, message ads) and CTAs across an industry keyword.
- **Spend proxies**: impression ranges and country split for ads delivered in the EU.
- **Targeting intelligence**: which targeting dimensions (company, job, audience, demographics…) an advertiser uses, and which locations and languages.
- **Lead generation**: find companies actively advertising on LinkedIn for a keyword, with their company page URL.

### Why this scraper?

- **Measured reliability**: all 36 test runs on the current build succeeded (100%), covering advertiser, keyword, payer, pasted search/detail URLs, multi-input, country (DE/FR/NL), date-range, card-only and 200-ad runs. Across all 110 development runs, 105 (95.5%) returned their data; the 5 failures were all on earlier builds, fixed since (see Limits). The fixes:
  - Chrome-only browser fingerprints, so Firefox or Safari fingerprints don't get blocked.
  - Automatic session rotation on 403/429.
  - Pagination that resumes from the same page.
  - Partial results are saved, never thrown away.
- **Per-input status**: the `OUTPUT` record gives each input one status: `SUCCEEDED`, `PARTIAL`, `NO_DATA`, `FAILED` or `DUPLICATE`. It also gives counts, errors and network stats, so you know exactly what you got.
- **Full ad text and details**: it opens each ad's detail page (optional), so ad text is not cut off at "…see more".
- **Clean, flat schema**: numbers are parsed (`impressionsMin`/`impressionsMax`), dates are ISO (`YYYY-MM-DD`) and tracking parameters are stripped from URLs.
- **Fair pricing**: you pay per ad, and the detail-page enrichment is charged only when it actually loaded.

### How to use it

1. Enter one or more **advertiser names** (e.g. `HubSpot`), **keywords** (e.g. `CRM`) or **paid-for-by** names. Each entry is a separate search.
2. Optionally restrict **countries** (ISO codes, e.g. `US`, `DE`) and the **date range**.
3. Set **Max ads per search** (LinkedIn serves 24 per page).
4. Leave **Include ad details** on for full data, or switch it off for a fast, cheap card-only scrape.
5. Run. Results go to the dataset; the per-input summary goes to the `OUTPUT` record in the key-value store.

You can also paste LinkedIn Ad Library **search URLs** or **single ad URLs** (`https://www.linkedin.com/ad-library/detail/<id>`).

### Input example

```json
{
  "advertisers": ["HubSpot", "Salesforce"],
  "keywords": ["CRM"],
  "countries": ["US", "DE"],
  "dateRange": "last-30-days",
  "maxAdsPerSearch": 100,
  "includeDetails": true
}
```

| Field | Description |
|---|---|
| `advertisers` | Advertiser (company or person) names; LinkedIn's "Advertiser name" filter. |
| `keywords` | Keywords contained in the ad. |
| `keywordFilter` | Optional keyword that is combined with each advertiser search. |
| `payers` | "Paid for by" names. |
| `adLibraryUrls` | LinkedIn Ad Library search or ad detail URLs. |
| `countries` | ISO 3166 alpha-2 codes; empty means all countries. Unknown codes are reported in `OUTPUT.inputErrors`. |
| `dateRange` | `all`, `last-30-days`, `current-month`, `current-year`, `last-year` or `custom` (with `startDate` / `endDate`). LinkedIn allows custom ranges within the last 12 months only; older start dates are clamped. |
| `maxAdsPerSearch` | 1–5000, default 50. |
| `includeDetails` | Default `true`. |
| `maxConcurrency` | Parallel browser sessions; default 5. |
| `maxRetries` | Retries per page, each in a new session and IP; default 10. |
| `proxyConfiguration` | Default Apify residential proxy (US). Datacenter proxies get blocked. |

### What data you get

The fill rates below were **measured on 3,360 ads from our test runs** (October 2026). There are three columns:

- **All ads (with details)**: 1,960 ads.
- **EU-delivered**: 766 ads, the ones that have transparency data.
- **Card-only** (`includeDetails: false`): 1,400 ads.

LinkedIn publishes run dates, impressions and targeting **only for ads that were shown in the EU** (DSA transparency). That is why those fields are filled for about 39% of ads overall and almost 100% of EU-delivered ads. For ads shown only outside the EU they are `null`, and that is LinkedIn's data, not a scraping failure.

| Field | All ads (details) | EU-delivered | Card-only | Notes |
|---|---|---|---|---|
| `adId`, `adUrl`, `searchInput`, `searchType`, `detailsStatus`, `scrapedAt` | 100% | 100% | 100% | |
| `advertiserName` | 100% | 100% | 99% | |
| `advertiserUrl`, `advertiserType` | 98% | 98% | 0% | Company page (`company`) or member profile (`person`). Detail page only. |
| `advertiserLogoUrl` | 94% | 93% | 92% | |
| `paidBy` | 100% | 100% | 0% | "Paid for by" entity. Detail page only. |
| `format` | 100% | 100% | 99% | E.g. Single Image Ad, Video Ad, Document Ad, Carousel Ad, Message Ad, LinkedIn Article Ad, Event Ad, Job Ad, Spotlight Ad, Follow Company Ad. |
| `creativeType` | 100% | 100% | 100% | LinkedIn's internal type, e.g. `SPONSORED_VIDEO`. |
| `adText` | 99% | 99% | 99% | Full text with details. Cards are usually cut off (`adTextTruncated: true`). |
| `messageSender` | 100% of message ads | | | Sender shown on Message/InMail ads. |
| `headline` | 54% | 63% | 58% | Only formats that have a headline. |
| `ctaText` | 54% | 63% | 0% | E.g. "Learn more", "Register", "Download the Guide". |
| `destinationUrl` | 34% | 79% | 0% | Landing page (tracking `trk` parameter removed). LinkedIn often omits it for ads not shown in the EU. |
| `imageUrls` | 93% | 96% | 93% | Image, carousel, document pages and video cover. Text-only ads have none. |
| `videoUrl` | 100% of video ads (20% of all) | | 0% | Highest-bitrate MP4. |
| `carouselCardCount` | 100% of multi-card ads (5%) | | 0% | |
| `firstShown`, `lastShown` | 39% | 100% | 0% | From "Ran from … to …". |
| `totalImpressions`, `impressionsMin`, `impressionsMax` | 39% | 100% | 0% | E.g. "20k-30k" → 20000 / 30000; "> 1M" → min 1000000, max null; "< 1k" → 0 / 1000. |
| `impressionsByCountry` | 39% | 100% | 0% | `[{country, percent, percentLabel}]`; `percent` is `null` when LinkedIn shows "< 1%". |
| `targetingLanguages`, `targetingLocationsIncluded` | 39% | 99–100% | 0% | |
| `targetingLocationsExcluded` | 2% | 4% | 0% | Only when the advertiser excluded locations. |
| `targetingParameters` | 39% | 100% | 0% | `[{parameter, targeted, excluded}]` for Audience, Demographic, Company, Education, Job, Member Interests and Traits. LinkedIn only reveals **whether** each was used, not the values. |

`detailsStatus` can be:

- `OK`: the detail page was parsed.
- `NOT_REQUESTED`: details were switched off.
- `NOT_AVAILABLE`: the ad was removed or has no detail page.
- `FAILED`: the detail page could not be loaded after all retries. The card data is still returned.

Not available from LinkedIn and therefore **not** in the output: spend amounts, exact impressions, the targeting values themselves (e.g. which job titles), and ad engagement.

### Output example

```json
{
  "adId": "1611644514",
  "adUrl": "https://www.linkedin.com/ad-library/detail/1611644514",
  "searchInput": "HubSpot",
  "searchType": "advertiser",
  "advertiserName": "HubSpot for Developers",
  "advertiserUrl": "https://www.linkedin.com/company/104750988",
  "advertiserType": "company",
  "advertiserLogoUrl": "https://media.licdn.com/dms/image/v2/.../hubspot_for_developers_logo",
  "paidBy": "HubSpot, Inc.",
  "format": "Single Image Ad",
  "creativeType": "SPONSORED_STATUS_UPDATE",
  "adText": "Less guesswork. More step-by-step 👀\n\nOne week until our Agent Builder Workshop!! ...",
  "adTextTruncated": false,
  "messageSender": null,
  "headline": "3.0 Agent Builder Workshop",
  "ctaText": null,
  "destinationUrl": "https://bit.ly/4yzZ7Sc",
  "imageUrls": ["https://media.licdn.com/dms/image/v2/D4E10AQHRTgYZFiIaDQ/image-shrink_1280/..."],
  "videoUrl": null,
  "carouselCardCount": null,
  "firstShown": "2026-10-23",
  "lastShown": "2026-10-26",
  "totalImpressions": "> 1M",
  "impressionsMin": 1000000,
  "impressionsMax": null,
  "impressionsByCountry": [
    { "country": "Colombia", "percent": 96, "percentLabel": "96%" },
    { "country": "Mexico", "percent": 3, "percentLabel": "3%" },
    { "country": "United States", "percent": null, "percentLabel": "<1%" }
  ],
  "targetingLanguages": ["English"],
  "targetingLocationsIncluded": ["Germany", "Netherlands", "United States", "Canada", "Mexico", "Ireland", "Colombia", "United Kingdom"],
  "targetingLocationsExcluded": null,
  "targetingParameters": [
    { "parameter": "Audience", "targeted": false, "excluded": false },
    { "parameter": "Company", "targeted": true, "excluded": false },
    { "parameter": "Job", "targeted": false, "excluded": true }
  ],
  "detailsStatus": "OK",
  "scrapedAt": "2026-10-26T16:44:10.512Z"
}
```

#### Run summary (`OUTPUT` record)

```json
{
  "status": "SUCCEEDED",
  "totalAds": 100,
  "searches": [
    { "input": "Salesforce", "type": "advertiser", "status": "SUCCEEDED", "ads": 100, "adsWithDetails": 100, "detailFailures": 0, "pagesLoaded": 5, "reachedEnd": false, "errors": [] }
  ],
  "network": { "pageNavigations": 10, "fragmentFetches": 4, "detailFetches": 91, "http429": 0, "http403": 0, "sessionsRetired": 0 }
}
```

If every input fails, the run is marked as failed, so you are not left with an empty "successful" run.

### Pricing

Pay per event, with no subscription:

| Event | Price | When |
|---|---|---|
| Actor start | $0.0025 per GB of memory ($0.005 at the default 2 GB) | Once per run |
| Ad | $0.0008 | Every ad in the dataset |
| Ad details | $0.0007 | Only for ads whose detail page loaded (`detailsStatus: "OK"`) |

That is **$1.50 per 1,000 ads with full details**, or **$0.80 per 1,000 ads card-only**, plus the start fee. Searches with no results cost only the start fee. Apify's "Maximum cost per run" setting is respected: the scraper stops cleanly when it is reached.

Examples: 50 ads with details cost $0.005 + 50 × $0.0015 = **$0.08**; 1,000 ads with details cost about **$1.51**; 200 ads card-only cost about **$0.17**.

Typical runs from our tests (with details):

- 30–50 ads: about 55–65 s (median 55 s; slowest 128 s).
- 200 ads: about 2.5 minutes at concurrency 8.
- 1,000 ads: about 12 minutes at concurrency 8.
- Card-only (`includeDetails: false`): 24 ads in 5–30 s, 200 ads in about 35 s.

### FAQ

**Do I need a LinkedIn account or cookies?** No. The Ad Library is public, and the scraper never logs in.

**Why are dates and impressions empty for some ads?** LinkedIn only publishes them for ads delivered in the EU. Use the **countries** filter with EU countries (e.g. `DE`, `FR`) if you need those fields for most ads.

**How far back can I search?** "All time" returns everything LinkedIn still lists. Custom date ranges are limited to the last 12 months by LinkedIn.

**Can I scrape one specific ad?** Yes. Put its `https://www.linkedin.com/ad-library/detail/<id>` URL in `adLibraryUrls`.

**How many ads can a search return?** As many as LinkedIn serves. We tested up to 1,000 per search; the input allows up to 5,000.

### Limits

- **Proxy**: residential proxy is required. Datacenter IPs are blocked by Cloudflare.
- **Pacing**: LinkedIn rate-limits fast clients, so detail pages are paced (about 2 s apart per session). Higher `maxConcurrency` uses more parallel sessions and is faster.
- **Advertiser search**: the advertiser filter is LinkedIn's own fuzzy name match, so a search for "Google" can also return ads from pages such as "Google Cloud".
- **Location lists**: targeted locations are given by LinkedIn as one comma-separated text. We split it into a list and re-attach region codes (e.g. "London, ON"), but names that themselves contain commas may be split.
- **Test failures**: 5 of our 110 development runs returned no data, all on earlier builds: one search blocked 7 times in a row, and 4 runs on one build whose first page was blocked by Cloudflare on every retry. The current build starts a fresh browser (new fingerprint) after each block in addition to a new IP; all 36 runs on it succeeded. Cloudflare can still occasionally block a search; that run is then marked `FAILED` with the reason in `OUTPUT`, and you pay only the start fee.

# Actor input Schema

## `advertisers` (type: `array`):

Search ads by advertiser (company or person) name, e.g. "HubSpot". Each entry is one search. Matches LinkedIn's "Advertiser name" filter.

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

Search ads whose text contains these keywords, e.g. "CRM". Each entry is one search.

## `keywordFilter` (type: `string`):

Optional. Only return ads from the advertisers above that also contain this keyword.

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

Optional. Search by the "Paid for by" entity (the payer), which can differ from the advertiser.

## `adLibraryUrls` (type: `array`):

Optional. Paste search URLs (https://www.linkedin.com/ad-library/search?...) or single ad URLs (https://www.linkedin.com/ad-library/detail/123...). Country/date filters below are not applied to pasted URLs.

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

Only ads shown in these countries (ISO 3166 alpha-2 codes, e.g. US, DE, GB). Leave empty for all countries.

## `dateRange` (type: `string`):

When the ads ran. LinkedIn only supports the last 12 months for custom ranges.

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

YYYY-MM-DD. Used only when Date range = Custom. Dates older than 12 months are moved to the earliest allowed date.

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

YYYY-MM-DD. Defaults to today.

## `maxAdsPerSearch` (type: `integer`):

Stop each search after this many ads (LinkedIn returns 24 per page).

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

Open each ad's detail page to get full ad text, CTA, landing URL, video URL, run dates, impressions, per-country impression share and targeting. Turn off for a faster, card-only scrape (text may be truncated, no dates/impressions/targeting).

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

Parallel browser sessions. Higher is faster but raises the chance of LinkedIn rate limits.

## `maxRetries` (type: `integer`):

Each retry uses a fresh browser session and residential IP.

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

Residential proxy is required - LinkedIn blocks datacenter IPs.

## Actor input object example

```json
{
  "advertisers": [
    "HubSpot"
  ],
  "dateRange": "all",
  "maxAdsPerSearch": 50,
  "includeDetails": true,
  "maxConcurrency": 5,
  "maxRetries": 10,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `ads` (type: `string`):

No description

## `summary` (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 = {
    "advertisers": [
        "HubSpot"
    ],
    "maxAdsPerSearch": 50,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("atlasdata/linkedin-ad-library-scraper-reliable").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 = {
    "advertisers": ["HubSpot"],
    "maxAdsPerSearch": 50,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("atlasdata/linkedin-ad-library-scraper-reliable").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 '{
  "advertisers": [
    "HubSpot"
  ],
  "maxAdsPerSearch": 50,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call atlasdata/linkedin-ad-library-scraper-reliable --silent --output-dataset

```

## MCP server setup

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

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/1daZt77ji8oGJw8L8/builds/xpVM0zbRpdCJ7hydz/openapi.json
