# LinkedIn Ad Library Keyword & Advertiser Scraper (`nimbryxo-studio/linkedin-ad-library-keyword-and-advertiser-scraper`) Actor

Search the public LinkedIn Ad Library by keyword, advertiser, country, or date range, and get headline, body copy, advertiser info, media URLs, and call-to-action for the ads each search returns.

- **URL**: https://apify.com/nimbryxo-studio/linkedin-ad-library-keyword-and-advertiser-scraper.md
- **Developed by:** [Nimbryxo-studio](https://apify.com/nimbryxo-studio) (community)
- **Categories:** Social media, Lead generation, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.49 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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 Keyword & Advertiser Scraper

Search the **public LinkedIn Ad Library** without an account, cookies, or a login. Collect paginated ads by keyword, advertiser name, payer, exact company ID, or a pasted Ad Library URL — then enrich each ad with its full copy, CTA, landing URL, video, paying entity, and the EU transparency block (run dates, impression range, country shares, and targeting).

The actor is built for competitor research, creative monitoring, and B2B ad intelligence. It reads the same public pages a logged-out visitor sees, so it works with zero credentials.

### Modes

| Mode | What it does | Inputs it uses |
| --- | --- | --- |
| `search` (default) | Builds one search per keyword / advertiser / payer / company-ID group, paginates it, dedupes by ad ID, and optionally opens every detail page. | `searchTerms`, `advertisers`, `companyIds`, `companyUrls`, `payer`, filters |
| `adDetails` | Re-scrapes specific ads from their IDs or detail URLs. | `adIds`, `directUrls` (detail URLs) |
| `directUrls` | Accepts pasted Ad Library search URLs, company URLs, and ad detail URLs. Search filters are preserved. | `directUrls` |
| `count` | Returns the total number of matching ads per search, without pagination. | The same search inputs |

Every keyword, advertiser, payer, and company-ID group is an **independent search** (a union). Filters such as country, date, sort, impressions, and targeting apply to all of them.

### Inputs

| Input | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | Select | `search` | `search`, `adDetails`, `directUrls`, or `count`. |
| `searchTerms` | String list | `[]` | Keyword searches. |
| `advertisers` | String list | `[]` | Advertiser names (fuzzy) or LinkedIn company URLs. |
| `companyIds` | String list | `[]` | Exact numeric LinkedIn company IDs; multiple IDs are OR-ed. URLs are accepted. |
| `companyUrls` | String list | `[]` | Company page URLs, resolved to their numeric organization ID. |
| `payer` | String list | `[]` | Legal entity that paid for the ad. |
| `advertiserMatch` | Select | `startsWith` | Client-side precision for advertiser-name searches: `any`, `startsWith`, `exact`. |
| `countries` | String list | `[]` | ISO-3166 alpha-2 codes (`US`, `GB`, `DE`) or `ALL`. Multiple codes are comma-joined. |
| `dateOption` | Select | `last-30-days` | `any-time`, `last-30-days`, `current-month`, `current-year`, `last-year`, `custom-date-range`. |
| `startDate` / `endDate` | String | — | Required for `custom-date-range` (YYYY-MM-DD, within the last 12 months). |
| `sortOrder` | Select | `newest` | `newest` or `oldest`. |
| `impressionsMinValue` / `impressionsMaxValue` | Integer | — | EU impression range. LinkedIn requires both values. |
| `impressionsMinUnit` / `impressionsMaxUnit` | Select | `none` | `none`, `thousand`, or `million`. |
| `targetingIncluded` / `targetingExcluded` | String list | `[]` | EU targeting facets: `LANGUAGE`, `LOCATION`, `AUDIENCE`, `DEMOGRAPHIC`, `COMPANY`, `EDUCATION`, `JOB`, `INTERESTS_AND_TRAITS`. |
| `includeDetails` | Boolean | `true` | Open each ad's detail page for full copy, CTA, landing URL, video, payer, and EU data. |
| `decodeShortLinks` | Boolean | `false` | Resolve `lnkd.in` and similar short links to the final landing URL. |
| `adIds` | String list | `[]` | Ad IDs or detail URLs, for `adDetails`. |
| `directUrls` | URL list | `[]` | Pasted Ad Library search/detail URLs or company URLs. |
| `maxItems` | Integer | `50` | Global cap per run (1–4,800). |

#### Prefill

The default prefill is the smallest run with a reliable result: Microsoft's exact company ID, the last 30 days, three ads, and detail enrichment on.

```json
{
  "mode": "search",
  "companyIds": ["1035"],
  "dateOption": "last-30-days",
  "includeDetails": true,
  "maxItems": 3
}
```

#### Example — keyword search, card data only

```json
{
  "searchTerms": ["AI", "machine learning"],
  "countries": ["US", "DE"],
  "dateOption": "last-30-days",
  "includeDetails": false,
  "maxItems": 100
}
```

#### Example — exact advertiser with EU transparency

```json
{
  "companyIds": ["1035"],
  "countries": ["DE"],
  "dateOption": "last-year",
  "includeDetails": true,
  "maxItems": 200
}
```

#### Example — re-scrape specific ads

```json
{
  "mode": "adDetails",
  "adIds": ["1611810664", "https://www.linkedin.com/ad-library/detail/1658527466"]
}
```

#### Example — count only

```json
{
  "mode": "count",
  "searchTerms": ["AI"],
  "countries": ["US"],
  "dateOption": "last-30-days"
}
```

### Output

Each ad is one dataset row with `recordType: "ad"`. Count mode emits `recordType: "count"` rows. Empty values are removed at every depth — there are no `null`, empty-string, empty-array, or sentinel rows.

#### Ad fields

| Field | Description |
| --- | --- |
| `ad_id` | LinkedIn ad / creative ID. |
| `ad_url` | Public Ad Library detail URL. |
| `recordType` | `ad`. |
| `sourceUrl` | The search URL or detail URL that produced the row. |
| `scrapedAt` / `scraped_at` | ISO 8601 UTC scrape timestamp (`scraped_at` is the legacy alias). |
| `advertiser_name`, `advertiser_id`, `advertiser_url`, `advertiser_logo`, `advertiser_tagline` | Advertiser identity. `advertiser_id` is a numeric company ID when available, otherwise a member/showcase slug. |
| `paid_for_by` | Paying legal entity from the detail page. |
| `ad_format`, `creative_type`, `format_label` | Normalized format (`image`, `video`, `carousel`, `document`, `message`, `spotlight`, `job`, `event`, `article`, `follow_company`, `dynamic`, `text`) plus the raw creative type and LinkedIn's label. |
| `headline` | Ad headline when present. |
| `body_text` | Copy preview from the search card (or the full preview when scraping a detail URL directly). |
| `body_text_full` | Full detail-page copy (up to roughly 2,700 characters). |
| `cta_text` | CTA button text (`Learn more`, `Register`, ...). |
| `landing_url`, `landing_url_raw`, `landing_domain`, `is_shortlink` | Cleaned landing URL (only the `trk` parameter is removed), original URL, hostname, and short-link flag. |
| `media` | List of `{type, url, expiresAt}` entries (`image`, `poster`, `video`). |
| `media_urls`, `image_urls`, `video_urls`, `video_poster_url` | Flat media lists for compatibility and convenience. |
| `start_date`, `end_date`, `duration_days`, `is_active` | Run dates and duration from the EU transparency block. `is_active` is only present when an end date exists. |
| `total_impressions`, `impressions_min`, `impressions_max` | Impression range label and parsed bounds. |
| `impressions_by_country` | `{country, shareRaw, sharePct, isUpperBound?, isLowerBound?, estimatedMin?, estimatedMax?}` rows. |
| `targeting_language`, `targeting_location_included`, `targeting_location_excluded`, `targeting_parameters` | EU targeting data. Parameters are `{category, targeted, excluded}` rows. |
| `has_dsa_data` | Always a boolean. `false` means LinkedIn did not publish the EU transparency block for that ad. |
| `country`, `top_country` | The single delivery-country filter when one is set; the largest impression-share country from the DSA block. |
| `variants_count` | Number of creative variants on the detail page. |

#### Count fields

| Field | Description |
| --- | --- |
| `recordType` | `count`. |
| `sourceUrl` | The search URL used. |
| `scrapedAt` / `scraped_at` | ISO timestamp. |
| `query`, `queryType`, `queryValue` | The search label, e.g. `keyword`, `advertiser`, `payer`, `companyIds`, `filters`. |
| `totalCount` | LinkedIn's reported number of matching ads. |

### Pagination, dedupe, and limits

- The Ad Library returns **24 ads per page** and exposes a pagination token.
- The actor follows up to **200 pages (4,800 ads) per search**, then stops. This is the practical ceiling the Ad Library accepts.
- Results are deduplicated by `ad_id` across pages and searches.
- `maxItems` is a global cap across all searches (1–4,800). It trims the final page exactly.
- Broad searches can match hundreds of thousands of ads. Slice them by country, date window, keyword, or exact company IDs to reach more than 4,800 rows.
- `maxItems` limits rows, not requests: with `includeDetails: true`, one detail request is made per collected ad (up to the cap).

### Reliability and errors

- The actor uses `curl_cffi` with a Chrome 131 TLS fingerprint and a **fresh Apify Proxy AUTO session/IP for every request** (sticky sessions fail when they land on a blocked IP). Each request is retried up to five times with backoff.
- If AUTO stays blocked, the actor tries a headless browser on AUTO, then the Apify UNBLOCKER group for that request. Proxy setup is automatic; there is no proxy input.
- Partial runs are reported honestly in the status message. If some searches are blocked and others return data, the dataset keeps the good rows and the status says how many queries were blocked.
- If every request is blocked, or a search that should return data comes back empty, the run **fails with a clear message**. It never pushes a fake "blocked" row.
- Detail-page failures (404/410 or a transient block) keep the card record and log the failure.
- A legitimate `0 results` search also fails the run with an explicit "0 results" message rather than silently succeeding with an empty dataset.
- A run summary (pages, details, query statuses) is saved to the key-value store as `run_summary`.

### FAQ

**Do I need a LinkedIn account or cookies?**
No. The Ad Library is public guest content. The actor makes the same anonymous requests the public site does.

**Do I need to configure a proxy?**
No. The actor selects and rotates its proxy automatically.

**Why do some ads lack impressions, dates, or targeting?**
LinkedIn only publishes that transparency block for ads served in the EU. Non-EU ads have no run dates, impression range, country shares, or targeting data. The `has_dsa_data` flag tells you which is which. This is a source limitation, not a scraper bug.

**Why are media URLs signed?**
LinkedIn CDN URLs carry an `e=` expiry parameter. The actor exposes the URL and its `expiresAt` date in `media[].expiresAt`. Download or archive media before it expires if you need a permanent copy.

**What is the difference between `advertisers` and `companyIds`?**
`advertisers` is a fuzzy name search and can include partners or resellers. `companyIds` is exact and is the best way to collect only a specific company's ads.

**Why is `body_text` shorter than `body_text_full`?**
The search card shows a truncated preview. `body_text_full` comes from the detail page and is only present when `includeDetails` is on (or when the mode is `adDetails`).

**How do I get more than 4,800 ads?**
Split the request by country, date range, keyword, or company ID and run each slice separately. Deduplicate downstream by `ad_id` if slices overlap.

**Can I see spend, budget, CTR, or conversions?**
No. LinkedIn does not publish those values in the Ad Library. Impressions are ranges, not exact counts.

**Is this affiliated with LinkedIn?**
No. This is an independent third-party actor that reads LinkedIn's public Ad Library. It is not affiliated with or endorsed by LinkedIn Corporation.

### Limitations

- Only public Ad Library ads are available; Campaign Manager data is not accessible.
- EU transparency data (dates, impressions, country shares, targeting) is published only for EU-served ads.
- LinkedIn reports impression ranges rather than exact impressions.
- Each search is capped at 4,800 ads (200 pages × 24).
- Media URLs are signed and expire; `expiresAt` is provided for archival planning.
- Per-ad delivery country is not published. `country` reflects a single-country search filter; `top_country` is the largest DSA share.
- Carousel/document slide text and non-EU active status are not published by the source.
- LinkedIn may rate-limit aggressive runs; the actor retries and reports partial results rather than fabricating rows.

# Actor input Schema

## `mode` (type: `string`):

What to collect: search the library, open specific ads, process pasted URLs, or count matches only.

## `searchTerms` (type: `array`):

One or more keyword searches, e.g. 'AI' or 'cloud computing'. Each keyword is an independent search (union), not a combination.

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

Advertiser names (fuzzy accountOwner search) or LinkedIn company URLs. Each name is an independent search. Use companyIds for exact matching.

## `companyIds` (type: `array`):

Exact numeric LinkedIn company IDs (for example, Microsoft is 1035). Multiple IDs are OR-ed in one exact search. Company URLs are accepted too.

## `companyUrls` (type: `array`):

LinkedIn company page URLs. The actor resolves each public page to its numeric organization ID and searches it exactly.

## `payer` (type: `array`):

Search ads by the legal entity that paid for them, e.g. 'Microsoft Corporation'. Each payer is an independent search.

## `advertiserMatch` (type: `string`):

Client-side precision for advertiser-name searches. Exact company IDs are never filtered. 'Starts with' removes partner-noise results while keeping names such as 'Microsoft in Business'.

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

ISO-3166 alpha-2 delivery country codes (US, GB, DE, ...) or ALL. Multiple codes are sent in one comma-separated filter.

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

LinkedIn's date window. Use custom-date-range with startDate/endDate for an exact period (maximum 12 months back).

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

Start of a custom range (YYYY-MM-DD). Required when dateOption is custom-date-range.

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

End of a custom range (YYYY-MM-DD). Required when dateOption is custom-date-range.

## `sortOrder` (type: `string`):

Newest first or oldest first.

## `impressionsMinValue` (type: `integer`):

Lower bound of the impression range (EU transparency data only). LinkedIn requires both a minimum and a maximum value.

## `impressionsMinUnit` (type: `string`):

Unit for the minimum impression value.

## `impressionsMaxValue` (type: `integer`):

Upper bound of the impression range (EU transparency data only). Required together with the minimum value.

## `impressionsMaxUnit` (type: `string`):

Unit for the maximum impression value.

## `targetingIncluded` (type: `array`):

Filters ads that target any of these categories (EU transparency data only). Values: LANGUAGE, LOCATION, AUDIENCE, DEMOGRAPHIC, COMPANY, EDUCATION, JOB, INTERESTS_AND_TRAITS.

## `targetingExcluded` (type: `array`):

Filters ads that exclude any of these categories (EU transparency data only). Values: LANGUAGE, LOCATION, AUDIENCE, DEMOGRAPHIC, COMPANY, EDUCATION, JOB, INTERESTS_AND_TRAITS.

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

Open each ad's detail page for full copy, CTA, landing URL, video, payer, and EU transparency data. Adds one request per ad.

## `decodeShortLinks` (type: `boolean`):

Resolve lnkd.in and similar short links to their final landing URL. Adds one request per short link.

## `adIds` (type: `array`):

Numeric ad IDs or linkedin.com/ad-library/detail/... URLs. Used by mode adDetails.

## `directUrls` (type: `array`):

Pasted Ad Library search URLs, ad detail URLs, or LinkedIn company page URLs. Search filters are preserved.

## `maxItems` (type: `integer`):

Global cap on ads returned across all searches (1-4800). Each search can return at most 4,800 ads (200 pages of 24).

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "AI",
    "cloud computing"
  ],
  "advertisers": [
    "Microsoft",
    "Anthropic"
  ],
  "companyIds": [
    "1035",
    "3608"
  ],
  "companyUrls": [
    "https://www.linkedin.com/company/microsoft"
  ],
  "payer": [
    "Microsoft Corporation"
  ],
  "advertiserMatch": "startsWith",
  "countries": [
    "US",
    "DE"
  ],
  "dateOption": "last-30-days",
  "sortOrder": "newest",
  "impressionsMinUnit": "none",
  "impressionsMaxUnit": "none",
  "targetingIncluded": [
    "LANGUAGE",
    "JOB"
  ],
  "targetingExcluded": [
    "COMPANY"
  ],
  "includeDetails": true,
  "decodeShortLinks": false,
  "adIds": [
    "1611810664"
  ],
  "directUrls": [
    {
      "url": "https://www.linkedin.com/ad-library/search?keyword=AI&countries=US"
    }
  ],
  "maxItems": 3
}
```

# Actor output Schema

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

All scraped ad records and count rows. Use recordType to separate them.

# 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 = {
    "mode": "search",
    "companyIds": [
        "1035"
    ],
    "dateOption": "last-30-days",
    "includeDetails": true,
    "maxItems": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("nimbryxo-studio/linkedin-ad-library-keyword-and-advertiser-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 = {
    "mode": "search",
    "companyIds": ["1035"],
    "dateOption": "last-30-days",
    "includeDetails": True,
    "maxItems": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("nimbryxo-studio/linkedin-ad-library-keyword-and-advertiser-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 '{
  "mode": "search",
  "companyIds": [
    "1035"
  ],
  "dateOption": "last-30-days",
  "includeDetails": true,
  "maxItems": 3
}' |
apify call nimbryxo-studio/linkedin-ad-library-keyword-and-advertiser-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nimbryxo-studio/linkedin-ad-library-keyword-and-advertiser-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/c4vabXjZcq0Eylrer/builds/wrUve4WlpfpUTcaYq/openapi.json
