# LinkedIn Ads Scraper (`solidcode/linkedin-ads-scraper`) Actor

\[💰 $0.5 / 1K] Extract ads from LinkedIn's public Ad Library: advertiser, ad copy, format, image and ad URL on every run, headline when present. Search by keyword, advertiser, country and date range. Detail mode adds destination link and paying entity. Dates, impressions and targeting are EU-only.

- **URL**: https://apify.com/solidcode/linkedin-ads-scraper.md
- **Developed by:** [SolidCode](https://apify.com/solidcode) (community)
- **Categories:** Social media, Lead generation, Other
- **Features:** Limited permissions
- **Stats:** 53 total users, 5 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 advertisement without details

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 Ads Scraper

Extract competitor ads from LinkedIn's public Ad Library at scale. Every ad comes back with its advertiser, creative format and Ad Library link, plus whatever copy and creative image LinkedIn puts on the card. Switch on detail mode and, in every market including the US, you also get the complete untruncated copy, the destination link, the paying legal entity and the video file on video ads. On EU-served ads LinkedIn publishes more on top: dates active, total impressions, each country's share of those impressions, and the advertiser's full targeting. Built for B2B marketers, competitive-intelligence analysts and agencies who need to see what rivals are running on LinkedIn without buying an enterprise ad-intelligence seat.

### Why This Scraper?

- **12 named ad formats, zero `unknown` rows across 2,112 test rows** — single image, video, carousel, document, message, spotlight, text, thought-leader, article, event, job and follow-company ads, each tagged with a clean `format` value you can filter and pivot on, and each matching the label LinkedIn itself prints on the card
- **42 countries to filter by** — US, UK, Canada, Ireland, Australia, Germany, France, Spain, Italy, Netherlands, Japan, South Korea, Brazil, Mexico, India, UAE, Saudi Arabia, Turkey, Israel and 23 more
- **Complete ad copy in every market, not the 121-character preview** — LinkedIn cuts body text off at 121 characters on search results. Detail mode returns the whole thing, up to 2,999 characters measured on a **US-only** run, and it works exactly the same in the US as it does in Europe
- **EU transparency data on top, decoded** — for ads LinkedIn discloses under the Digital Services Act you additionally get dates active, total impressions, each country's percentage share of them, and the full targeting picture: languages, included locations, excluded locations, and six audience facets (Audience, Demographic, Company, Education, Job, Member Interests and Traits). Measured at 100% fill on an EU detail run
- **Three search modes, combinable in one run** — keyword, advertiser company slug, or a pasted Ad Library URL. Stack dozens together; rows are deduplicated by ad ID so the same ad is never returned or charged twice
- **Advertiser searches return one company, not a brand's whole ecosystem** — asking for `hubspot` gives you HubSpot's own ads, not the 7 partner agencies with "HubSpot" in their company name. One switch turns the wider ecosystem view back on when you want it
- **Paused and expired campaigns included** — LinkedIn keeps roughly 12 months of history, so you see what a competitor stopped running, not only what is live today
- **Creative assets, not just text** — 94.9% of rows on a broad test search carried the creative image URL, and detail mode adds the direct video file URL on video ads, ready to drop into a swipe file or a creative-review deck. Job and message ads are the exception, because LinkedIn ships no creative at all on those
- **Two-tier pay-per-event pricing** — $0.50 per 1,000 ads. Only the ads that actually carried the EU transparency disclosure cost $0.90 per 1,000, so on a measured US-only detail run 72.5% of rows still billed at the basic price. Nothing else is charged

### Use Cases

**Competitive Intelligence**

- Monitor every ad your competitors run on LinkedIn, including campaigns they have already paused
- Track which messaging, headlines and calls to action your industry is testing this quarter
- Spot new product launches by watching a competitor's ad volume spike

**Ad Creative Research**

- Pull thousands of ad creatives by topic ("AI", "cybersecurity", "fintech") for inspiration
- Build a swipe file of high-performing B2B ad copy across all 12 formats
- Compare image vs video vs document strategies in your niche

**Brand Monitoring**

- Watch for competitors bidding against your brand keywords
- Detect partner or reseller ads that misrepresent your product
- Audit your own agency's live campaigns from the public viewer

**Lead Generation & Sales Intelligence**

- Identify which companies are actively running paid LinkedIn campaigns, a strong intent signal
- Build prospect lists of advertisers in specific verticals or geographies
- Surface agencies and SaaS vendors actively investing in growth

**Market & Trend Research**

- Quantify advertising activity by geography or industry over time
- Map which message frames dominate B2B marketing right now
- Feed datasets into BI dashboards for client-facing reporting

### Getting Started

#### Example A — Simple keyword search

The fastest way to start. Each keyword runs its own search and the results are merged and deduplicated.

```json
{
    "keywords": ["marketing automation", "sales enablement"],
    "maxResults": 500
}
```

#### Example B — Advertiser-scoped, multi-country, with date filter

Pull every ad a competitor ran in the US and UK over the last year. The slug is the part after `linkedin.com/company/` in their company URL.

```json
{
    "advertisers": ["hubspot", "salesforce"],
    "countries": ["US", "GB"],
    "dateOption": "last-year",
    "maxResults": 2000
}
```

#### Example C — Detail-enriched run

Switch on `fetchDetails` to open each ad's own page. In any country that gives you the complete ad copy, the destination link, the paying entity and the video file on video ads.

```json
{
    "keywords": ["b2b saas"],
    "countries": ["US"],
    "dateOption": "last-30-days",
    "fetchDetails": true,
    "maxResults": 1000
}
```

Swap `countries` to EU markets on the same input and LinkedIn's transparency block fills in as well: dates active, total impressions, per-country impression shares and complete targeting.

```json
{
    "keywords": ["b2b saas"],
    "countries": ["DE", "FR", "NL", "ES", "IT"],
    "dateOption": "last-30-days",
    "fetchDetails": true,
    "maxResults": 1000
}
```

#### Example D — Bulk via Start URLs

Already built a search in the LinkedIn Ad Library interface? Paste the URL directly and every filter is preserved.

```json
{
    "startUrls": [
        "https://www.linkedin.com/ad-library/search?keyword=cybersecurity&countries=US",
        "https://www.linkedin.com/ad-library/search?accountOwner=nvidia&dateOption=last-year"
    ],
    "maxResults": 5000
}
```

#### Example E — Custom date range

Pull ads active in a specific six-week window. Both dates must fall within the last 12 months, which is LinkedIn's retention limit.

```json
{
    "keywords": ["generative ai"],
    "dateOption": "custom-date-range",
    "startDate": "2026-02-01",
    "endDate": "2026-03-15",
    "maxResults": 1000
}
```

### Input Reference

#### What to search

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `keywords` | string\[] | empty | Free-text search terms (e.g. "AI marketing", "crypto"). Each keyword runs its own search and the results are combined |
| `advertisers` | string\[] | empty | LinkedIn company slugs (e.g. "nvidia", "microsoft", "hubspot"). The slug comes from the company URL — `linkedin.com/company/<slug>`. Returns only that company's own ads. Use the slug from the URL, not a display name like "Microsoft Corp" |
| `includeRelatedAdvertisers` | boolean | `false` | Widen an advertiser search back out to a company-*name* match, so partners, resellers and agencies carrying the brand in their own name are included too (searching "hubspot" then also returns "Systony - HubSpot Elite Partner" and similar). You are billed for those extra ads |
| `startUrls` | string\[] | empty | Paste full LinkedIn Ad Library search URLs directly. Bypasses the keyword and advertiser fields above |

#### Filters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `countries` | string\[] | empty | Restrict results to ads served in selected countries. 42 supported (US, GB, CA, DE, FR, JP, BR, IN, AE and more). Empty means every country |
| `dateOption` | string | `last-30-days` | Time window. One of `last-30-days`, `current-month`, `current-year`, `last-year`, `custom-date-range` |
| `startDate` | string | empty | Custom start date, `YYYY-MM-DD`. Used only when `dateOption` is `custom-date-range`. Must fall within the last 12 months |
| `endDate` | string | empty | Custom end date, `YYYY-MM-DD`. Used only when `dateOption` is `custom-date-range`. Must fall within the last 12 months |

#### Output options and limits

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `fetchDetails` | boolean | `false` | Opens each ad's own page. **In every country** that returns the complete body copy instead of the 121-character preview, the destination link, the paying entity, the advertiser profile link and the video file on video ads. **On EU-served ads only**, LinkedIn additionally publishes dates active, total impressions, impressions per country and targeting. Only the ads carrying that EU disclosure are billed at the higher tier |
| `maxResults` | integer | `1000` | Cap on total ads across all searches combined. Leave the box empty and the run stops at 1,000. Enter `0` for no cap. Each individual search still stops at roughly 4,800 ads, which is how deep LinkedIn lets a single search go |

### Output

Each run pushes one row per ad. Two record shapes are possible: a basic search-result record, and (when `fetchDetails: true`) a record enriched from the ad's own page.

#### Basic record (`fetchDetails: false`)

```json
{
    "recordType": "ad",
    "adId": "1281881684",
    "adLibraryUrl": "https://www.linkedin.com/ad-library/detail/1281881684",
    "advertiserName": "Maxio",
    "advertiserSlug": null,
    "advertiserUrl": null,
    "advertiserLogoUrl": "https://media.licdn.com/dms/image/v2/C4E0BAQE361-JB8cBLA/company-logo_100_100/0/1649952212473/wearemaxio_logo",
    "paidBy": null,
    "format": "image",
    "creativeType": "SPONSORED_STATUS_UPDATE",
    "headline": "Download Maxio Institute's Latest Report",
    "headlineDescription": null,
    "body": "In the realm of SaaS, benchmarks are more than just numbers — they're game-changers. Here's why...",
    "imageUrl": "https://media.licdn.com/dms/image/v2/D5610AQFymJR7uHth1w/image-shrink_1280/B56Z3FO9k9HQAc-/0/1777130527273",
    "videoUrl": null,
    "clickUrl": null,
    "country": null,
    "firstSeen": null,
    "lastSeen": null,
    "impressionsRange": null,
    "impressionsPerCountry": null,
    "targeting": null,
    "detailPageFetched": false,
    "detailsFetched": false,
    "query": "keyword: marketing automation",
    "scrapedAt": "2026-04-27T15:42:01Z"
}
```

#### Detail-enriched record (EU ad, `fetchDetails: true`)

```json
{
    "recordType": "ad",
    "adId": "1389159976",
    "adLibraryUrl": "https://www.linkedin.com/ad-library/detail/1389159976",
    "advertiserName": "Niketa Calame-Harris, MFA",
    "advertiserSlug": "niketacalame",
    "advertiserUrl": "https://www.linkedin.com/in/niketacalame",
    "advertiserLogoUrl": "https://media.licdn.com/dms/image/v2/D4D03AQ.../profile-displayphoto.jpg",
    "paidBy": "The Harris Tech Pivot",
    "format": "image",
    "creativeType": "SPONSORED_STATUS_UPDATE",
    "headline": "Welcome to The Harris Tech Pivot",
    "headlineDescription": null,
    "body": "Bridging the gap between underrepresented talent and tech opportunity. Full ad copy comes back here, often well past 1,000 characters, with paragraph breaks preserved.",
    "imageUrl": "https://media.licdn.com/dms/image/v2/D4D22AQ.../feedshare-shrink_2048_1536.jpg",
    "videoUrl": null,
    "clickUrl": "https://harristechpivot.example.com/welcome",
    "country": null,
    "firstSeen": "2026-04-10",
    "lastSeen": "2026-04-14",
    "impressionsRange": {"min": 1000, "max": 5000, "label": "1k-5k"},
    "impressionsPerCountry": {
        "Germany": {"percent": 62.0, "percentMin": 62.0, "percentMax": 62.0, "label": "62%"},
        "France": {"percent": 37.0, "percentMin": 37.0, "percentMax": 37.0, "label": "37%"},
        "Poland": {"percent": 1.0, "percentMin": 0.0, "percentMax": 1.0, "label": "less than 1%"}
    },
    "targeting": {
        "languages": ["English"],
        "locations": ["City of Johannesburg", "United States"],
        "locationsExcluded": ["India", "Europe", "Accra"],
        "parameters": {
            "Audience": {"targeted": false, "excluded": false},
            "Demographic": {"targeted": true, "excluded": false},
            "Company": {"targeted": false, "excluded": false},
            "Education": {"targeted": false, "excluded": false},
            "Job": {"targeted": true, "excluded": false},
            "Member Interests and Traits": {"targeted": false, "excluded": false}
        }
    },
    "detailPageFetched": true,
    "detailsFetched": true,
    "query": "advertiser: nike",
    "scrapedAt": "2026-04-27T15:42:14Z"
}
```

#### Ad content fields

Returned on every run, whatever the settings. `recordType`, `adId`, `adLibraryUrl` and `format` came back on 100% of 2,112 test rows. The creative fields depend on what LinkedIn puts on the card, so read the coverage notes before building a pipeline that assumes they are always there.

| Field | Type | Description |
|-------|------|-------------|
| `recordType` | string | Always `"ad"`. Discriminator for downstream pipelines |
| `adId` | string | LinkedIn's unique ad identifier |
| `adLibraryUrl` | string | Public Ad Library page for this ad |
| `format` | string | One of `image`, `video`, `carousel`, `document`, `message`, `spotlight`, `text`, `thoughtleader`, `article`, `event`, `job`, `follow`. Each maps to the label LinkedIn prints on the card itself ("Single Image Ad", "Video Ad", "Follow Company Ad" and so on). `unknown` appears only if LinkedIn ships a creative type nobody has seen before, and it did not appear on any of 2,112 test rows |
| `creativeType` | string | null | LinkedIn's internal creative type (e.g. `SPONSORED_STATUS_UPDATE`, `SPONSORED_VIDEO`), preserved as-is for advanced filtering |
| `headline` | string | null | Primary headline. **Do not assume it is present.** LinkedIn omits the headline block on search results for article, carousel, message and document ads and most video ads, so fill rate follows the format mix your search happens to return: **40% to 91% across eight test searches** |
| `headlineDescription` | string | null | Secondary headline line. Often `null`; LinkedIn rarely renders one |
| `body` | string | null | Ad body copy, paragraph breaks preserved. Present on **96% to 100%** of rows. Search results carry a 121-character preview ending in `…`; switch on `fetchDetails` for the complete copy, in any country. Job and message ads carry no copy block at all and return `null` |
| `imageUrl` | string | null | Ad creative image URL. Present on most ads (94.9% on a broad test search). Job and message ads ship no creative and return `null` |
| `videoUrl` | string | null | Direct video file URL, highest quality available. LinkedIn publishes it on the ad's own page only, so this needs `fetchDetails` switched on, and it fills on video ads |

#### Advertiser fields

| Field | Type | Description |
|-------|------|-------------|
| `advertiserName` | string | Display name of the advertiser, company or individual member |
| `advertiserLogoUrl` | string | null | Advertiser's logo or profile image |
| `advertiserSlug` | string | null | LinkedIn identifier for the advertiser, either a numeric company id (`"68529"`) or a member vanity slug (`"niketacalame"`). Filled on **every row of an Advertisers search**, with no need for `fetchDetails`. Keyword and pasted-URL searches fill it from the ad's own page, so those need `fetchDetails: true` |
| `advertiserUrl` | string | null | Full LinkedIn URL of the advertiser's company or member page. Same rule as `advertiserSlug`: free on an Advertisers search, needs `fetchDetails: true` for keyword and pasted-URL searches |
| `paidBy` | string | null | Legal entity that paid for the ad. **Needs `fetchDetails: true`**, then present on nearly every ad |

#### Detail-page fields

All of these are `null` unless `fetchDetails` is switched on. `clickUrl` and `detailPageFetched` come back in **every** country, as do `paidBy`, `videoUrl` and the full `body` covered above. The five marked **EU only** additionally require LinkedIn to have published its Digital Services Act disclosure for that ad, which it does for EU-served ads and not for US or other non-EU ads. On a measured US search 27.5% of enriched rows carried that disclosure; on an EU search it was 100%.

| Field | Type | Description |
|-------|------|-------------|
| `clickUrl` | string | null | External destination the ad links to. Present on ads that point somewhere outside LinkedIn; job, event and follow-company ads keep the user on LinkedIn and return `null` |
| `firstSeen` | string | null | **EU only.** First date the ad ran, `YYYY-MM-DD` |
| `lastSeen` | string | null | **EU only.** Last date the ad ran, `YYYY-MM-DD`, or `null` if it is still running |
| `impressionsRange` | object | null | **EU only.** Total impressions bucket as `{"min": int, "max": int, "label": str}`, e.g. `{"min": 10000, "max": 20000, "label": "10k-20k"}`. The smallest bucket comes back as `{"min": 0, "max": 1000, "label": "< 1k"}`; an open-ended top bucket has `"max": null` |
| `impressionsPerCountry` | object | null | **EU only.** Each country's share of the ad's total impressions: `{"Germany": {"percent": 62.0, "percentMin": 62.0, "percentMax": 62.0, "label": "62%"}}`. `percent` is the sortable number LinkedIn shows, and `label` keeps its own wording. LinkedIn groups every country below one percent into a single `"less than 1%"` bucket that it still reports as 1, so add up `percentMin` for the lowest possible share and `percentMax` for the highest — see the tip below |
| `targeting` | object | null | **EU only.** `{ languages: string[], locations: string[], locationsExcluded: string[], parameters: { "<facet>": {"targeted": bool, "excluded": bool} } }`. `locations` are the regions the advertiser targeted, `locationsExcluded` the ones they ruled out. Facet keys come from LinkedIn's own labels: `Audience`, `Demographic`, `Company`, `Education`, `Job`, `Member Interests and Traits` |
| `detailPageFetched` | boolean | `true` when the ad's own page was fetched and folded into this row, which is what fills `clickUrl`, `paidBy`, `videoUrl` and the full `body`. Always `false` when `fetchDetails` is off. **Filter on this one to find enriched rows** |
| `detailsFetched` | boolean | `true` when this ad carried the EU disclosure, so the higher price tier applied. `false` otherwise, and always `false` when `fetchDetails` is off. Note this is narrower than `detailPageFetched`: a non-EU ad can be fully enriched and still read `false` here, because LinkedIn publishes no disclosure for it |

#### Run metadata

| Field | Type | Description |
|-------|------|-------------|
| `country` | string | null | Reserved for future use, currently always `null`. LinkedIn does not tag individual ads with a country; the `countries` input is a search-level filter |
| `query` | string | Which of your inputs produced this row, e.g. `"keyword: marketing automation"`, `"advertiser: hubspot"`, `"URL: https://..."` |
| `scrapedAt` | string | ISO-8601 UTC timestamp when the row was captured |

### Tips for Best Results

#### Detail mode is worth switching on outside the EU too

The complete ad copy, the destination link, the paying entity and the video file come back in every country. On a US-only test run, detail mode lifted the longest ad body from 121 characters to 2,999 and filled the paying entity on 100% of rows. The EU-only part is the transparency block: dates active, total impressions, per-country impression shares and targeting. That came back on 27.5% of rows in a US search and 100% of rows in an EU one, so set `countries` to EU markets (DE, FR, NL, ES, IT, BE, PT, SE, DK, FI, PL, AT, IE) when the transparency block is specifically what you are after.

#### Expect job and message ads to arrive bare

LinkedIn's search results render job ads and message (InMail) ads as a stripped-down card with no headline, no body copy and no creative image, so those three fields come back `null`. They were 2.2% of rows across eight test searches, and as high as 9.3% on a search dominated by one heavy job-ad advertiser. Filter on `format` to drop `job` and `message` rows if empty text columns would break a downstream pipeline.

#### Use the slug from the company URL, not the display name

An advertiser search is pinned to one company page, so `hubspot` returns HubSpot's ads and not the partner agencies with "HubSpot" in their name. That pinning works off the slug in `linkedin.com/company/<slug>`, so copy it from the URL bar. If a brand can't be pinned the run says so in the log and falls back to a name match for that one advertiser. Set `includeRelatedAdvertisers: true` when the partner ecosystem is what you actually want to map.

#### Cast a wider net with several keywords

Results are ranked by relevance, so related terms surface different ads. Try `"marketing automation"`, `"marketing platform"` and `"demand generation"` in one run. Rows are deduplicated by ad ID across all your searches, so overlapping terms never cost you twice.

#### Filter formats after the run

LinkedIn's Ad Library has no format filter, so every run returns all 12 formats. Use the `format` field to narrow to just videos, just carousels, or just document ads once the data is in. Turn on detail mode for a video-only pull and each row also brings back the video file URL.

#### Don't add up the country percentages

LinkedIn reports every country below one percent as a single "less than 1%" bucket but still prints it as 1, so a widely served ad can list 140 countries and appear to total 200%. Add up `percentMin` instead for the smallest share those countries can really represent, and `percentMax` for the largest. The two together bracket the true split, and they also tell a genuine 1% market apart from a rounding-error one.

#### Watch the 12-month wall on custom ranges

LinkedIn retains roughly 12 months of ad history. A `startDate` older than that is pulled forward to the earliest available date, and `custom-date-range` needs both `startDate` and `endDate` to be set.

#### Size big jobs around the 4,800-ad ceiling

A single search stops at roughly 4,800 ads, as deep as LinkedIn will page one query, so for 10,000 ads or more split a broad term into narrower keyword or date variants rather than raising `maxResults`. Results also arrive 24 at a time and the last batch is always kept whole, so expect up to 23 rows more than your cap instead of a truncated page.

### Pricing

**From $0.50 per 1,000 ads.** Pay per ad returned, with no compute charges. Switching on detail mode does not move every ad to the higher price: only the ads LinkedIn published its EU transparency disclosure for cost more. On a measured US-only detail run, 27.5% of rows hit the detail price and the other 72.5% stayed at the basic price, so the full ad copy, destination link and paying entity came back on all of them for close to the basic rate.

Discount-tier pricing starts on **October 25, 2026**: Gold subscribers keep today's prices, while Bronze, Silver and No-discount accounts pay the slightly higher rates below. Until then every account pays $0.50 per 1,000 basic ads and $0.90 per 1,000 detail ads.

#### Basic ads — `fetchDetails` off, or an ad with no EU disclosure

| Ads returned | No discount | Bronze | Silver | Gold |
|--------------|-------------|--------|--------|------|
| 100 | $0.060 | $0.056 | $0.053 | $0.050 |
| 1,000 | $0.60 | $0.56 | $0.53 | $0.50 |
| 10,000 | $6.00 | $5.60 | $5.30 | $5.00 |
| 100,000 | $60.00 | $56.00 | $53.00 | $50.00 |

#### Detail ads — `fetchDetails` on and the ad carried the EU disclosure

| Ads returned | No discount | Bronze | Silver | Gold |
|--------------|-------------|--------|--------|------|
| 100 | $0.108 | $0.102 | $0.096 | $0.090 |
| 1,000 | $1.08 | $1.02 | $0.96 | $0.90 |
| 10,000 | $10.80 | $10.20 | $9.60 | $9.00 |
| 100,000 | $108.00 | $102.00 | $96.00 | $90.00 |

A mixed run is billed ad by ad: each EU-disclosed ad at the detail price, everything else at the basic price. You are never charged the higher price for an ad that came back without the extra data.

### Integrations

Export results in JSON, CSV, Excel, XML or RSS. Connect to 1,500+ apps via:

- **Zapier** / **Make** / **n8n** — workflow automation
- **Google Sheets** — direct spreadsheet export
- **Slack** / **Email** — notifications on new results
- **Webhooks** — push results to your own systems
- **Apify REST API** — full programmatic access from any backend

### Legal & Ethical Use

This actor is built for legitimate market research, competitive intelligence, brand monitoring and ad creative analysis. The data it collects comes from LinkedIn's **public Ad Library**, a transparency portal LinkedIn publishes for exactly this purpose. You are responsible for complying with applicable laws and LinkedIn's Terms of Service. Do not use the data for spam, harassment, deceptive comparative advertising, or any illegal purpose.

# Changelog

This Actor's version history is a separate document: https://apify.com/solidcode/linkedin-ads-scraper/changelog.md

# Actor input Schema

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

Free-text search terms (e.g. "AI marketing", "crypto"). Each keyword runs its own search and the results are combined. Leave empty to search by advertiser or by direct URLs.

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

LinkedIn company slugs (e.g. "nvidia", "microsoft", "hubspot"). The slug comes from the LinkedIn company URL — `linkedin.com/company/<slug>`. Each advertiser runs its own search and returns **only that company's own ads** — partners, resellers and agencies with the same brand in their company name are excluded. Use the slug exactly as it appears in the URL; display names like "Microsoft Corp" are not deterministic and may return zero results.

## `includeRelatedAdvertisers` (type: `boolean`):

Off by default: an advertiser search returns only that company's own ads. Switch this on to go back to matching on company *name*, which also pulls in resellers, partners and agencies that carry the brand in their own name (searching "hubspot" then also returns "Systony - HubSpot Elite Partner", "Avidly HubSpot Solutions" and similar). Useful for mapping a partner ecosystem, wasteful if you're tracking one competitor — you're billed for those extra ads too. Only affects the Advertisers field.

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

Paste full LinkedIn Ad Library search URLs directly. Useful when you've built a search in the LinkedIn UI and want the same results. Bypasses the keyword and advertiser fields above.

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

Restrict results to ads served in these countries. Leave empty to include every country.

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

Time window for when the ad was active. LinkedIn's Ad Library only retains ~12 months of history regardless of the option chosen.

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

Only used when 'Date range' is set to 'Custom date range'. Format YYYY-MM-DD. Must be within the last 12 months.

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

Only used when 'Date range' is set to 'Custom date range'. Format YYYY-MM-DD. Must be within the last 12 months.

## `fetchDetails` (type: `boolean`):

Opens each ad's own page to collect what the search results don't carry. **Worth switching on in any country, not just the EU.** For every ad, anywhere: the complete body copy instead of the 121-character preview (measured up to 2,999 characters on a US-only run), the destination link, the paying entity, the advertiser's LinkedIn profile link, and the direct video file on video ads. **EU-served ads only:** LinkedIn additionally publishes dates active, total impressions, impressions per country and full targeting (languages, locations, audience facets) — those stay empty on US and other non-EU ads no matter what. Only the ads carrying that EU disclosure are billed at $0.90 per 1,000 instead of $0.50 per 1,000; on a measured US-only detail run 72.5% of rows stayed at the basic price. Leave off if the search-result fields are enough.

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

Cap the total number of ads returned across all queries combined. Leave the box empty and the run stops at 1,000 ads — type 0 if you want no cap at all. Either way each individual search is capped at roughly 4,800 ads (200 pages × 24) by LinkedIn's pagination depth, and the actor stops when it reaches that limit. Pages contain 24 ads each, so the actor may overshoot your cap by up to 23 to keep the last page intact.

## Actor input object example

```json
{
  "keywords": [
    "marketing automation"
  ],
  "advertisers": [],
  "includeRelatedAdvertisers": false,
  "startUrls": [],
  "countries": [
    "US"
  ],
  "dateOption": "last-30-days",
  "startDate": "",
  "endDate": "",
  "fetchDetails": false,
  "maxResults": 1000
}
```

# Actor output Schema

## `overview` (type: `string`):

Table of scraped ads with advertiser, format, headline, ad copy and creative.

## `details` (type: `string`):

Detail-page columns, empty unless 'Fetch full ad details' is switched on: full ad copy, paying entity, destination link and video file in every country, plus dates active, impressions, impressions per country and targeting on EU-served ads.

# 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": [
        "marketing automation"
    ],
    "advertisers": [],
    "includeRelatedAdvertisers": false,
    "startUrls": [],
    "countries": [
        "US"
    ],
    "dateOption": "last-30-days",
    "startDate": "",
    "endDate": "",
    "fetchDetails": false,
    "maxResults": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("solidcode/linkedin-ads-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": ["marketing automation"],
    "advertisers": [],
    "includeRelatedAdvertisers": False,
    "startUrls": [],
    "countries": ["US"],
    "dateOption": "last-30-days",
    "startDate": "",
    "endDate": "",
    "fetchDetails": False,
    "maxResults": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("solidcode/linkedin-ads-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 '{
  "keywords": [
    "marketing automation"
  ],
  "advertisers": [],
  "includeRelatedAdvertisers": false,
  "startUrls": [],
  "countries": [
    "US"
  ],
  "dateOption": "last-30-days",
  "startDate": "",
  "endDate": "",
  "fetchDetails": false,
  "maxResults": 1000
}' |
apify call solidcode/linkedin-ads-scraper --silent --output-dataset

```

## MCP server setup

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