# LinkedIn Ad Library Scraper: Ads, Media & Targeting (`whoareyouanas/linkedin-ad-library-scraper`) Actor

Scrape LinkedIn Ad Library by company, keyword or ad URL. Get ad copy, images, videos, payer, dates and available targeting. Export unique ads as JSON, CSV or Excel. No LinkedIn login needed.

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

## Pricing

from $0.70 / 1,000 full 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

Scrape public LinkedIn ads by company, advertiser name, keyword, or ad URL. Get ad copy, images, video links, carousel cards, and the details LinkedIn shows about the payer, dates, impressions, and targeting.

Use the results for competitor ad research, creative reviews, campaign research, or AI tools. No LinkedIn account or cookies needed. Export to JSON, CSV, or Excel, or use the Apify API in your own workflow.

### What you get

- **Exact company searches.** Use a company ID or company URL. The Actor checks the advertiser before returning a match.
- **Two price options.** Choose search cards for a lower cost, or full mode for available ad details.
- **Several searches in one run.** Each unique ad is saved once. A separate report shows which searches found it.
- **Creative links and ad copy.** Get images, video files, ordered carousel cards, and available document previews.
- **Clear missing data and search limits.** See which fields LinkedIn did not show and whether a run reached its result, time, or request limit.

Full detail extraction has been tested live for **single image, video, carousel, document, and thought leader ads**. Other formats may return search card data with a warning. Document results include available preview data, which can be fewer pages than the source lists. The original PDF is not guaranteed.

### Quick start

1. Click **Try for free** and open the Actor input.
2. Add a company name, company URL, company ID, keyword, or direct ad URL.
3. Choose `listing` or `full` and set your result limits.
4. Start the run. Open the dataset to view or download the ads, and check the run summary for search coverage.

For an exact company search, paste this into the JSON input editor:

```json
{
  "companyIds": ["68529"],
  "mode": "full",
  "maxResults": 25,
  "maxResultsPerSearch": 25
}
```

`68529` is HubSpot's LinkedIn company ID. Keep all IDs as strings.

#### Search several companies or keywords

Each entry is a separate search. The global result limit counts unique ads across all searches.

```json
{
  "searches": [
    {
      "type": "companyUrl",
      "value": "https://www.linkedin.com/company/hubspot/",
      "label": "HubSpot exact company"
    },
    {
      "type": "advertiser",
      "value": "Apify"
    },
    {
      "type": "keyword",
      "value": "marketing automation",
      "filters": { "countries": ["DE"] }
    }
  ],
  "mode": "listing",
  "maxResults": 100,
  "maxResultsPerSearch": 50
}
```

#### Get specific ads by URL

```json
{
  "adUrls": [
    "https://www.linkedin.com/ad-library/detail/1578348084",
    "https://www.linkedin.com/ad-library/detail/1508101043"
  ],
  "mode": "full",
  "maxResults": 2,
  "allowListingFallback": false
}
```

These public URLs were tested during development. LinkedIn can change their availability. Direct ad URLs always request the detail page, even if `mode` is `listing`. Search filters do not apply to direct ad URLs.

### Listing or full mode?

| | `listing` | `full` |
|---|---|---|
| Best for | Finding ads and reviewing search cards | Reviewing available ad copy, creative details, and ad disclosures |
| Ad copy | Search card copy; may be shortened | Available detail-page copy; accepted full results are not shortened |
| Creative assets | Links shown on search cards | Available images, video variants, carousel cards, and document preview details |
| Payer, dates, impression ranges, targeting | Not requested from detail pages | Returned when LinkedIn shows them |
| Result price | Listing price | Full price for full results; listing price for a fallback |

`full` is the default. If a detail page fails or uses an unverified format, the default is to save useful search card data at the listing price. These rows have `retrievalLevel: "listing"` and a warning. Set `allowListingFallback: false` to skip them instead.

Exact company searches may need detail requests to check the advertiser even in listing mode. Direct URL results have no separate search card to fall back to.

### Choose the right search

| Input | Meaning |
|---|---|
| `companyIds` | Exact LinkedIn organization IDs, supplied as strings |
| `companies` | Advertiser names or public LinkedIn company URLs |
| `keywords` | Text searches across ad copy |
| `adUrls` | Specific public ad detail URLs |
| `searches` | Searches with a type, value, optional label, and optional filters |
| `startUrls` | Public LinkedIn Ad Library search or detail URLs, or company URLs |

Supported `searches[].type` values are `companyId`, `companyUrl`, `advertiser`, `keyword`, `searchUrl`, and `adUrl`.

**A company name is a text search.** It can match more than one advertiser. Use a company ID or company URL when you need an exact match. A company URL must resolve to a verified organization ID before that search can return exact matches.

You can combine the input arrays. Invalid individual searches appear in `RUN_SUMMARY.inputErrors`; other valid searches continue. Invalid global settings stop the run.

### Filters and limits

Use `filters` for all searches, or add `filters` to an individual `searches` entry to override them.

```json
{
  "companies": ["HubSpot"],
  "mode": "full",
  "filters": {
    "countries": ["DE", "FR"],
    "payer": "HubSpot",
    "dateRange": "custom",
    "startDate": "2026-09-01",
    "endDate": "2026-09-30"
  },
  "maxResults": 25
}
```

- `countries`: Two-letter country codes. This is LinkedIn's ad delivery filter, not the advertiser's headquarters.
- `payer`: Text search for the company or person paying for the ad.
- `dateRange`: `all-time`, `last-30-days`, `current-month`, `current-year`, `last-year`, or `custom`.
- `startDate` and `endDate`: Both required for custom dates, in `YYYY-MM-DD` form.
- `adFormats`: An optional filter applied after ads are read. Values include `SINGLE_IMAGE`, `VIDEO`, `CAROUSEL`, `DOCUMENT`, `TEXT`, `MESSAGE`, `CONVERSATION`, `THOUGHT_LEADER`, `EVENT`, `SPOTLIGHT`, and `EMPLOYER_BRAND`. A filter value does not mean that format has verified full detail support.

For `searchUrl` input, the URL's filters apply. Conflicting structured filters are rejected. Unsupported filters are also rejected.

| Setting | Default | What it controls |
|---|---:|---|
| `maxResults` | 100 | Maximum unique ads saved across the run |
| `maxResultsPerSearch` | 100 | Maximum unique matches for each search |
| `maxRequests` | 5,000 | HTTP attempts, including retries and company lookup |
| `maxRunSeconds` | 900 | Active run time in seconds |
| `maxConcurrency` | 8 | Maximum requests at once; allowed range is 1 to 20 |
| `allowListingFallback` | `true` | Save useful search cards when full details cannot be accepted |
| `includeEvidence` | `false` | Add a short source-field excerpt and hash for checking extraction |

Set either result limit to `0` to remove that row limit. Time, request, and spending limits still apply. `resultsLimit` is an alias for the **global** `maxResults`. `skipDetails: true` maps to `listing`; `false` maps to `full`. Conflicting aliases are rejected.

Apify cloud runs use datacenter proxy by default. `proxyConfiguration` can override this. LinkedIn can block requests or slow them down, so run times and available results can vary.

### Output fields

The dataset has one row per unique ad within a run. Uniqueness uses both `sourceKind` and `adId`. Repeated matches across searches do not create extra rows.

| Fields | What they contain |
|---|---|
| `adId`, `sourceKind`, `adUrl` | Ad ID, library type, and source ad link |
| `advertiserName`, `advertiserCompanyId`, `advertiserUrl`, `advertiserLogo` | Advertiser details |
| `contentAuthor`, `paidBy` | Source-shown author and payer, separate from the advertiser |
| `format`, `formatRaw` | Standard format label and LinkedIn's original label |
| `headline`, `body`, `bodyTruncated`, `ctas` | Ad text, whether the body is shortened, and button labels |
| `imageUrl`, `videoUrl`, `documentUrl`, `media` | Available creative links and media details |
| `carouselCards`, `document`, `creativeVariants` | Ordered cards, document preview details, and available variants |
| `clickUrl`, `landingPageUrl` | Destination links when the source shows them |
| `availability` | Source-shown `firstShown` and `lastShown` dates |
| `impressions`, `impressionsPerCountry` | Impression range and country shares, with original labels and parsed values |
| `targeting` | Source-shown audience settings, including included or excluded groups |
| `retrievalLevel`, `fieldStates`, `warnings` | Detail level, missing-field explanations, and issues to review |
| `sourceUrl`, `scrapedAt`, `schemaVersion` | Source link, collection time, and output version |

IDs remain strings. Video results keep available MP4 variants and use the highest bitrate as the main `videoUrl`. Media links can expire. Asset files are not downloaded.

#### Missing fields and data accuracy

Full mode means available detail fields passed the Actor's checks. It does not mean LinkedIn showed every field.

Missing values stay `null` or empty. `fieldStates` explains why:

| State | Meaning |
|---|---|
| `available` | The source showed the field |
| `not_exposed` | The page was read, but did not show the field |
| `not_requested` | This mode did not request the field |
| `restricted` | The source reported a restriction |
| `fetch_failed` | The source request failed |
| `parse_failed` | The response could not be read reliably |
| `unknown` | The field's status could not be established |

The Actor does not estimate ad spend, turn impression ranges into exact counts, or guess whether an ad is active from its last shown date. `activityStatus` remains `unknown` unless LinkedIn explicitly reports a restriction.

For documents, `document.previewComplete` and `document.originalDocumentComplete` show the limits of the returned assets. Available cover images and a source manifest link can be returned without a complete preview or the original PDF. The manifest is not fetched.

### Check search coverage

A successful Apify run can contain useful partial results. Check these key-value store records before treating the dataset as a complete collection:

| Record | What to check |
|---|---|
| `RUN_SUMMARY` | Counts, outcome, `inventoryComplete`, `detailsComplete`, limits, errors, and billing |
| `QUERY_RESULTS` | Matches, page count, exact company resolution, completeness, and stop reason for each search |
| `QUERY_MATCHES` | Which searches matched each saved ad |
| `RESOLUTION_RESULTS` | How company URLs were resolved to IDs |
| `HTTP_METRICS` | Request attempts, retries, blocks, and HTML bytes received |

`inventoryComplete: true` means the source confirmed the end of the requested search. Reaching `maxResults` means partial coverage, even when the run succeeded. For direct ad URLs, completeness refers to those URLs, not all ads from the advertiser. `detailsComplete` is a separate check on requested full details.

A confirmed no-match search returns zero rows and an empty outcome. A blocked or unreadable response is reported as an error, never as a confirmed empty search.

### API and AI agent use

Use the same JSON input through the Apify API. This example starts a small full-mode run and waits up to 60 seconds:

```bash
curl --request POST \
  'https://api.apify.com/v2/acts/whoareyouanas~linkedin-ad-library-scraper/runs?waitForFinish=60' \
  --header "Authorization: Bearer $APIFY_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{"companyIds":["68529"],"mode":"full","maxResults":5,"maxResultsPerSearch":5,"maxRunSeconds":90}'
```

The response includes `data.id`, `data.status`, `data.defaultDatasetId`, and `data.defaultKeyValueStoreId`. The run may still be running after the wait. Check `GET /v2/actor-runs/{runId}` until it finishes, then read:

```text
GET https://api.apify.com/v2/datasets/{defaultDatasetId}/items?format=json
GET https://api.apify.com/v2/key-value-stores/{defaultKeyValueStoreId}/records/RUN_SUMMARY
GET https://api.apify.com/v2/key-value-stores/{defaultKeyValueStoreId}/records/QUERY_RESULTS
```

Send the same authorization header with these requests. Use `format=csv` for a CSV export.

**For agents:** Keep IDs as strings. Use exact company IDs or company URLs for exact advertiser research. Set result and run limits. After each run, read `RUN_SUMMARY`, `QUERY_RESULTS`, `retrievalLevel`, and `fieldStates`. Treat missing fields as unknown data, and partial searches as partial coverage. Do not use empty arrays or old dates as proof that a company has no active ads.

### Pricing

Pay for unique ads saved, plus a small run-start charge. Standard in-run computing and the default proxy are included. Standard Apify storage charges can apply after the run.

| Apify price tier | Listing, per 1,000 ads | Full, per 1,000 ads |
|---|---:|---:|
| Free | $0.40 | $1.20 |
| Bronze | $0.35 | $1.00 |
| Silver | $0.30 | $0.85 |
| Gold, Platinum, Diamond | $0.25 | $0.70 |

The start charge is **$0.002 per memory-based start unit**. At the default 512 MB, a run uses one start unit, so the start charge is $0.002. At up to 1 GB, it is still one unit. Above 1 GB, Apify rounds the assigned memory up to whole GB and charges that many units.

Billing events are `ad-listing`, `ad-full`, and `apify-actor-start`. A saved ad is charged at one result level only. Full-mode rows that fall back to listing data use the listing price.

- Duplicate matches within the same run have no extra ad charge.
- Failed or skipped ads have no ad charge.
- Empty and failed runs still have the run-start charge.
- Each new run is billed separately, including ads saved in an earlier run.

For example, 1,000 accepted full results on the Free plan at the default memory cost **$1.202**, including one start unit. Check the Actor's Pricing tab for the current rates and set a spending limit before starting a large run. Custom proxies and services you supply can have their own costs.

### FAQ

#### Do I need a LinkedIn account?

No. The Actor reads the public LinkedIn Ad Library. It does not need your login or cookies.

#### Does it scrape LinkedIn profiles or contact details?

This Actor collects public ad library records. It does not collect member profiles, emails, or private LinkedIn data.

#### Can I get all ads from one company?

Use an exact company ID or company URL and choose suitable limits. The Actor follows search pages, but access blocks and budgets can stop the run. Check `inventoryComplete` and the per-search stop reason before calling the collection complete.

#### Are targeting and impression details always available?

No. Availability depends on what LinkedIn shows for that ad and region. Use full mode, then check `fieldStates` to distinguish missing source data from a request failure.

#### Can I download the original document or video?

The Actor returns available source links. Video ads can include direct MP4 links. Document ads may expose only covers and preview details. An original PDF is not promised, and files are not downloaded by the Actor.

#### Which formats have full detail support?

Single image, video, carousel, document, and thought leader layouts have passed live full-detail checks. Other formats can return listing data with warnings. Set `allowListingFallback: false` if you want accepted full results only.

#### How fast is it?

Speed depends on the number of ads, mode, and LinkedIn response times. Tested cloud runs returned 50 full ads in about 53 seconds and three direct video, carousel, and document ads in about 3 seconds. These are examples, not a speed guarantee.

#### Can I run it on a schedule?

Yes. Use Apify schedules to start new runs. Each run returns its own dataset; this Actor does not compare runs or create change alerts.

### Support

Open an issue on this Actor's Issues tab with the run ID and a description of the problem. Include the search type and expected result so the issue can be reproduced. Do not share account tokens or other secrets.

# Actor input Schema

## `searches` (type: `array`):

Objects with type (companyId, companyUrl, advertiser, keyword, searchUrl, adUrl), string value, optional label and per-search filters. Organization IDs must be strings. URL filters are authoritative; contradictory structured filters are rejected. Invalid searches are recorded while other valid searches continue.

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

Advertiser name text or a public LinkedIn company URL. Names are text matches; URL slugs must resolve to a verified organization ID.

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

Positive numeric organization IDs supplied as strings. Returned advertiser identity is verified before exact matches are delivered.

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

Independent text searches across ad copy.

## `adUrls` (type: `array`):

HTTPS public /ad-library/detail/<numeric-id> or /ad-library/employer-brand/<numeric-id> URLs. Search filters do not apply to direct ads.

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

Defaults to full in the Actor code. Listing records only source-visible card fields and marks truncated copy. Full additionally fetches each eligible unique detail. Missing source disclosure is not invented. Direct ad URLs always require full retrieval.

## `allowListingFallback` (type: `boolean`):

In full mode, deliver usable card data at the listing event price when detail retrieval fails. False withholds those ads and records their failures.

## `filters` (type: `object`):

Optional countries (ISO alpha-2 array), payer text, dateRange, startDate and endDate. No country constraint by default. Date presets: all-time, last-30-days, current-month, current-year, last-year, custom. Custom dates require both YYYY-MM-DD dates. Unknown filters are rejected; last-7-days and last-90-days are not currently advertised.

## `maxResults` (type: `integer`):

Global delivered-ad limit; defaults to 100 in the Actor code. Zero removes this row limit while request/time/charge budgets still apply.

## `maxResultsPerSearch` (type: `integer`):

Per-search unique usable match limit. Zero removes this row limit.

## `maxRequests` (type: `integer`):

Includes retries and company resolution. Reaching this budget produces an explicit partial summary.

## `maxRunSeconds` (type: `integer`):

Operational deadline in seconds. Reaching it produces an explicit partial summary.

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

Upper request ceiling. Host pacing can reduce actual concurrency.

## `adFormats` (type: `array`):

Optional canonical labels: SINGLE\_IMAGE, VIDEO, CAROUSEL, DOCUMENT, TEXT, MESSAGE, CONVERSATION, THOUGHT\_LEADER, EVENT, SPOTLIGHT, EMPLOYER\_BRAND. Filter runs after scanning. Full detail acceptance is verified for image, video, carousel, document and thought-leader layouts; other formats may return listing fallbacks.

## `includeEvidence` (type: `boolean`):

Preserve bounded source excerpts and hashes for extraction audits.

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

Standard Apify proxy settings. Cloud runs use datacenter proxy by default; explicit settings override it. Local development uses direct access by default.

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

Public LinkedIn search, company, standard detail, or employer-brand URLs. String URLs or {url} objects are accepted. Only GET is supported.

## `resultsLimit` (type: `integer`):

Alias of maxResults. Unlike Silva's per-URL setting, this alias is GLOBAL; use maxResultsPerSearch for per-URL limits. Conflicting aliases are rejected.

## `skipDetails` (type: `boolean`):

True maps to listing; false maps to full. Conflicting mode aliases are rejected.

## Actor input object example

```json
{
  "searches": [
    {
      "type": "companyId",
      "value": "68529"
    }
  ],
  "mode": "full",
  "allowListingFallback": true,
  "maxResults": 100,
  "maxResultsPerSearch": 100,
  "maxRequests": 5000,
  "maxRunSeconds": 900,
  "maxConcurrency": 8,
  "includeEvidence": false
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

## `queries` (type: `string`):

No description

## `matches` (type: `string`):

No description

## `resolutions` (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 = {
    "searches": [
        {
            "type": "companyId",
            "value": "68529"
        }
    ],
    "mode": "full",
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("whoareyouanas/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 = {
    "searches": [{
            "type": "companyId",
            "value": "68529",
        }],
    "mode": "full",
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("whoareyouanas/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 '{
  "searches": [
    {
      "type": "companyId",
      "value": "68529"
    }
  ],
  "mode": "full",
  "maxResults": 100
}' |
apify call whoareyouanas/linkedin-ad-library-scraper --silent --output-dataset

```

## MCP server setup

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