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

Scrape ads from the public LinkedIn Ad Library by advertiser, keyword, country and date: ad text, media, payer, run dates, impressions and targeting. No login or cookies. Pay only for ads returned.

- **URL**: https://apify.com/muhammad_noman_riaz/linkedin-ad-library-scraper.md
- **Developed by:** [Muhammad Noman Riaz](https://apify.com/muhammad_noman_riaz) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 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

### What does LinkedIn Ad Library Scraper do?

LinkedIn Ad Library Scraper collects ads from the public [LinkedIn Ad Library](https://www.linkedin.com/ad-library), the transparency site where LinkedIn lists ads that ran on its platform. Search by advertiser name, by keyword in the ad text, or paste Ad Library search and ad URLs. You can limit searches to countries and to a date range. For every ad you get the advertiser and its LinkedIn page, who paid for the ad, the ad format, the full ad text, headline, call to action, landing URL and image, video or carousel media URLs. When LinkedIn shows them, you also get the run dates, the total impressions range, the share of impressions per country and the targeting the advertiser picked (language, location and which audience parameters were used). It works without a LinkedIn account, cookies or login. Use it for competitor ad research, creative inspiration, B2B market research and lead lists of companies that advertise on LinkedIn.

### How to use it

1. Add one or more advertiser names (for example `HubSpot`), keywords, or Ad Library URLs.
2. Optionally pick countries and a date range.
3. Set **Max ads per search** and start the run.
4. Download the ads as JSON, CSV, Excel or HTML, or read them through the Apify API.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `advertisers` | array of strings | | Company or advertiser names. Each name is one search. |
| `searchKeywords` | array of strings | | Keywords in the ad text. Each keyword is one search. |
| `startUrls` | array of URLs | | Ad Library search URLs (`https://www.linkedin.com/ad-library/search?...`) and ad URLs (`https://www.linkedin.com/ad-library/detail/<id>`). Other URLs are ignored. |
| `countries` | array of strings | all | Two letter ISO codes such as `DE`, `FR`, `US`. Applies to advertiser and keyword searches. |
| `dateRange` | string | `all` | `all`, `last-30-days`, `current-month`, `current-year`, `last-year` or `custom-date-range`. |
| `startDate`, `endDate` | string | | `YYYY-MM-DD`, only used with `custom-date-range`. |
| `maxAdsPerSearch` | integer | 50 | Maximum ads per advertiser, keyword or search URL. |
| `includeDetails` | boolean | true | Open each ad page for payer, CTA, landing URL, run dates, impressions, countries and targeting. Charged extra only when details load (see Pricing). Off returns only the search card fields. |
| `maxConcurrency` | integer | 5 | Ad pages loaded at the same time. |
| `maxRequestRetries` | integer | 6 | Attempts per page, each on a new proxy IP. |
| `proxy` | object | Residential | Proxy settings. Residential proxies are recommended. |

#### Input example

```json
{
  "advertisers": ["HubSpot"],
  "searchKeywords": ["crm"],
  "countries": ["DE"],
  "dateRange": "last-30-days",
  "maxAdsPerSearch": 30,
  "includeDetails": true,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

### Output

One item per ad. This is a real item from a test run (advertiser `HubSpot`):

```json
{
  "adId": "1644380406",
  "adUrl": "https://www.linkedin.com/ad-library/detail/1644380406",
  "advertiserName": "HubSpot",
  "advertiserUrl": "https://www.linkedin.com/company/68529",
  "paidBy": "HubSpot, Inc.",
  "format": "video",
  "creativeType": "SPONSORED_VIDEO",
  "adText": "Your CRM data is more powerful than you think. Put it to work on Google and reach the people most likely to convert.",
  "headline": "Google Ads in HubSpot",
  "cta": "Learn more",
  "landingUrl": null,
  "media": [
    { "type": "video", "url": "https://dms.licdn.com/playlist/vid/v2/D5610AQH7YyKZCNgEGQ/progressive-servable-video/B56aE8kiTPHQAY-/0/1792017574172?e=2147483647&v=beta&t=5Sw4xFkvU8O1kHuPW9VxGFAkYcfQ-I41IRYmbbH_yfI" },
    { "type": "thumbnail", "url": "https://media.licdn.com/dms/image/v2/D5610AQH7YyKZCNgEGQ/videocover-high/B56aE8kiTPHQBI-/0/1792017574927?e=2147483647&v=beta&t=0_Sd0PO1AZLKwraUaBnVM4czcVHCUNybneAEFBD84DM" }
  ],
  "firstShown": null,
  "lastShown": null,
  "impressionsRange": null,
  "impressionsMin": null,
  "impressionsMax": null,
  "countries": [],
  "targeting": null,
  "searchQuery": "https://www.linkedin.com/ad-library/search?accountOwner=HubSpot",
  "scrapedAt": "2026-10-17T16:56:02.267Z"
}
```

When LinkedIn publishes run dates, impressions and targeting for an ad, the same item carries them. These fields come from another ad in the same run:

```json
{
  "firstShown": "2026-10-15",
  "lastShown": "2026-10-17",
  "impressionsRange": "1k-5k",
  "impressionsMin": 1000,
  "impressionsMax": 5000,
  "targeting": {
    "language": { "includes": "English" },
    "location": { "includes": "DACH" },
    "parameters": [
      { "parameter": "Audience", "targeted": false, "excluded": false },
      { "parameter": "Company", "targeted": true, "excluded": true },
      { "parameter": "Job", "targeted": true, "excluded": true }
    ]
  }
}
```

Country shares look like `{ "country": "Germany", "impressionsShare": "100%", "impressionsSharePercent": 100 }`. A share shown as `< 1%` keeps that text and has `impressionsSharePercent: null`.

Ads that could not be loaded are listed in the `FAILED_ITEMS` record of the run's key-value store, with the reason.

### Pricing

Two parts, so you only pay for what you get:

| What | Price per 1,000 ads |
|---|---|
| Every ad returned (search data: advertiser, format, text, headline, media) | $1.50 |
| Extra, only for ads whose details loaded (payer, CTA, landing URL, run dates, impressions, countries, targeting) | $3.50 |

A fully detailed ad costs $5 per 1,000. LinkedIn blocks some detail pages; those ads are still returned with `detailsLoaded: false` and cost only $1.50 per 1,000. Ads that could not be loaded at all are not charged.

### FAQ

#### Do I need a LinkedIn account?

No. The LinkedIn Ad Library is public and the scraper never logs in or uses cookies. If LinkedIn answers with a login wall or a challenge, the scraper retries on a new IP and never tries to get past it.

#### Which countries are covered?

Every country in the Ad Library country filter. Ads can be searched worldwide or limited with the `countries` input. LinkedIn shows run dates, impressions, country shares and targeting for some ads only. In our tests they appeared on ads served in Europe, while ads without them still came with advertiser, payer, format, text and media.

#### How far back does it go?

As far as the Ad Library goes. The custom date filter on the site accepts dates up to one year back. Searches without a date range return what LinkedIn lists, newest first.

#### Can I see ad spend?

No. LinkedIn does not publish spend in the Ad Library. You get the total impressions range and the share of impressions per country where LinkedIn shows them.

### Known limits

- Run dates, impressions, country shares and targeting are only returned when LinkedIn shows them on the ad page.
- `landingUrl` is only filled when the ad page links to an outside site. Many single image and video ads in the library have no clickable link.
- Location targeting is returned as LinkedIn's text (for example `"Richmond, VA, McLean, VA, Berlin"`), not split into a list, because place names contain commas.
- Ads with several versions return the first version shown.
- Document, event, spotlight, message and conversation ads have not been checked against live pages yet. Their common fields (advertiser, payer, format, text, run data) use the same page layout, but media may be incomplete.
- With `includeDetails` off, only advertiser name, format, shortened text, headline and media from the search card are returned.
- LinkedIn rate-limits fast traffic. Keep residential proxies on for runs beyond a few dozen ads.

# Actor input Schema

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

Company or advertiser names, as you would type them in the Ad Library 'Search by company/advertiser name' box. Each name is a separate search.

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

Keywords to find in ad text. Each keyword is a separate search.

## `startUrls` (type: `array`):

LinkedIn Ad Library search URLs (https://www.linkedin.com/ad-library/search?...) and ad URLs (https://www.linkedin.com/ad-library/detail/<id>). Search URLs keep their advertiser, keyword, payer, country and date filters.

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

Two letter ISO country codes (for example DE, FR, US) to limit keyword and advertiser searches to ads shown in those countries. Leave empty for all countries.

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

Limits keyword and advertiser searches to ads that ran in this period. Use custom-date-range with Start date and End date.

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

First day of a custom date range, as YYYY-MM-DD. LinkedIn accepts dates up to one year back.

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

Last day of a custom date range, as YYYY-MM-DD.

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

Maximum number of ads to return for each advertiser, keyword and search URL. Ad URLs always return one ad.

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

Opens each ad page for run dates, impressions, per-country reach and targeting. Charged as an extra event, only for ads whose details loaded. LinkedIn blocks some detail pages; those ads are still returned with search data (detailsLoaded: false) and only the base price is charged.

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

Maximum number of ad pages loaded at the same time.

## `maxRequestRetries` (type: `integer`):

How many times a page is tried, each time on a new proxy IP, before the ad or search is skipped.

## `proxy` (type: `object`):

Proxy used for requests. Residential proxies are recommended because LinkedIn blocks most datacenter IPs.

## Actor input object example

```json
{
  "advertisers": [
    "HubSpot"
  ],
  "dateRange": "all",
  "maxAdsPerSearch": 50,
  "includeDetails": true,
  "maxConcurrency": 5,
  "maxRequestRetries": 6,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One item per ad with advertiser, payer, format, text, media, run dates, impressions and targeting

# 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"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("muhammad_noman_riaz/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 = {
    "advertisers": ["HubSpot"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

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

```

## MCP server setup

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