# LinkedIn Ads Transcript Scraper — Ad Library Transcription (`steadyfetch/linkedin-ads-transcript-scraper`) Actor

LinkedIn Ad Library ads as text: video ads become transcripts with the first-3s hook, image ads return their on-image copy, plus CTA and advertiser data per ad. Search by advertiser, keyword or market — no cookies, no login. No start fee; text-free, silent and blocked ads are never charged.

- **URL**: https://apify.com/steadyfetch/linkedin-ads-transcript-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** AI, Social media, Videos
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 ad creative transcripts

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## LinkedIn Ads Transcript Scraper — Ad Library Transcription

**Give it an advertiser name or a keyword and get their LinkedIn Ad Library ads back as text.** Every video ad comes back with its **full transcript** and the **first-3-seconds hook**; every image ad with its **on-image copy** (headline, body, CTA); both with the advertiser, headline, ad text, CTA and paying entity, one JSON row per ad. **From $8.00 per 1,000 ad creative transcripts** on the Business plan ($20.00 on the free plan), platform usage included, no start fee. You are charged only when text lands in your dataset: creatives with nothing readable, blocked pages and failures cost $0. No cookies, no LinkedIn login.

**Just want to see it work?** Click **Start** with nothing set and the run is a 3-creative sample — a browse of the United States, last 30 days, read and charged like any run (about $0.06 on the free plan, $0.024 on Business). **Changed only an option** — the image-text or on-screen-text toggle, a cap, New ads only, a watchlist name — **and named nothing?** Those settings are applied to the same sample and the run goes ahead, charged like any run, with one uncharged `sample_note` row saying which settings were yours; a cap smaller than the sample is honoured exactly, a bigger one never grows it. You can also **browse a market's recent ads with no keyword at all** (country + date range), paste an Ad Library search link, or chain it straight after any LinkedIn Ad Library scraper run. **Running the same ads again?** An ad already delivered to your account is handed back from that run and not charged a second time — automatically, whatever mode found it, on any run that names what to read (the no-input sample always reads current ads). **Watching competitors over time?** Name a watchlist and re-run on a schedule: with New ads only you get only what is new (see [Watch competitors on a schedule](#watch-competitors-on-a-schedule--pay-only-for-new-ads)).

> **New: image ads are read too, and that changes your bill.** About 4 in 5 LinkedIn ads are static
> image ads, and each one whose copy is readable is now a charged result at the same price as a
> transcript. A keyword run that used to return a handful of video transcripts now returns roughly
> **ten times as many charged rows**. Use `maxItems` and the run's **max total charge** to set the
> size you want — or turn **Read the text on image ads** off to get the old video-only behaviour
> exactly as before.

| You give it | You get back |
|---|---|
| Advertiser names (e.g. `HubSpot`) | `transcript` — full speech-to-text, any length (first 3 minutes included, then a small per-minute surcharge) |
| …or keywords (e.g. `crm software`) | `hook3s` — the ad's opening line, plus `hookStartSeconds` |
| …or just a market — `countries: ["US"]` + a date range, **no keyword needed** | `imageText` — the headline, body and CTA read off an image ad |
| …or a pasted Ad Library **search link**, ad URLs or video URLs | `advertiser`, `headline`, `adText`, `payingEntity`, `language`, `durationSeconds`, timestamped `segments` |
| …or an Ad Library scraper run's **dataset ID** | one JSON row per ad, misses clearly marked and never charged |
| …plus a **watchlist name** (optional) | a separate list per competitor set — and, with **New ads only**, nothing you already paid for |
| …the same ads again, in any mode | ads already delivered to your account come back from that run, `repeat: true`, not charged |

### Output

One JSON row per ad. Delivered rows carry the transcript (or the creative's text) and `charged: true`; anything that could not be delivered carries a `status` saying why, and `charged: false`. One case delivers uncharged: if your max charge is reached while an ad is already being transcribed or read, that ad still ships in full with `charged: false` and a `statusReason` naming the cap — your cap is never exceeded, and work already done is never thrown away. Real rows from the live example run below (long text trimmed with … for the page):

| status | charged | advertiser | headline | hook3s | hookStartSeconds | transcript | language | durationSeconds |
|---|---|---|---|---|---|---|---|---|
| `transcribed` | `true` | Sparkon | HubSpot Elite Partner | Growth, AI, CRM Implementations, RevOps | We are Hubspot Elite Partner | If your revenue team is working harder than ever, but closing less, the problem isn't effort, it's the system. | 0 | If your revenue team is working harder than ever, but closing less, the problem isn't effort, it's the system. Too many manual tasks, too many disconnected tools… | English | 43.86 |
| `transcribed` | `true` | Sparkon | HubSpot Elite Partner | Growth, AI, CRM Implementations, RevOps | We are Hubspot Elite Partner | When your CRM becomes too complex, every team pays the price. | 0 | When your CRM becomes too complex, every team pays the price. We connect your financial systems, automate workflows, and give every team clear operational visibility… | English | 10.68 |
| `image_text_extracted` | `true` | HubSpot | *null* | *null* | *null* | *imageText:* "46% of marketers struggle with visual consistency. Get the free HubSpot & Canva Playbook" | *null* | *null* |
| `no_audio_stream` | `false` | HubSpot | *null* | *null* | *null* | *null* | *null* | *null* |

![LinkedIn ad transcripts output table — advertiser, headline, first-3s hook, transcript, language and the charged flag](https://api.apify.com/v2/key-value-stores/wciMDNpNgBh87mGUp/records/linkedin-ads-transcript-scraper--output-table.png)

Full row, as JSON:

```json
{
  "status": "transcribed",
  "charged": true,
  "advertiser": "Sparkon | HubSpot Elite Partner | Growth, AI, CRM Implementations, RevOps",
  "headline": "We are Hubspot Elite Partner",
  "adText": "Is your revenue team working harder, but closing less? ⚙️ …",
  "payingEntity": "Paid for by Triario LLC",
  "adId": "1508450294",
  "detailUrl": "https://www.linkedin.com/ad-library/detail/1508450294",
  "format": "SPONSORED_VIDEO",
  "availability": "Ran from Aug 17, 2026 to Aug 21, 2026",
  "impressions": "10k-20k",
  "impressionsByCountry": [{ "country": "Germany", "share": "70%" }, { "country": "Switzerland", "share": "22%" }],
  "hook3s": "If your revenue team is working harder than ever, but closing less,",
  "hookStartSeconds": 0,
  "transcript": "If your revenue team is working harder than ever, but closing less, the problem isn't effort, it's the system...",
  "language": "English",
  "durationSeconds": 43.9,
  "segments": [{ "start": 0, "end": 7.7, "text": "If your revenue team is working harder than ever, but closing less, the problem isn't effort, it's the system." }],
  "chargeEvents": { "transcript": 1, "surchargeMinutes": 0 }
}
```

`availability`, `impressions` and `impressionsByCountry` are LinkedIn's EU ad-transparency fields: they are populated only for ads that were shown in the EU (this sample ran in Germany and Switzerland) and are `null` on ads shown only elsewhere — a US-only campaign returns `null` in all three.

**[Browse the full live example dataset →](https://api.apify.com/v2/datasets/O0thSfah7LZve7qcM/items?clean=true\&format=json)** — real output from a verified run, including the uncharged rows. Export with `clean=true` for spreadsheet-ready CSV; reconcile your invoice from the per-row `charged` flags immediately, or from the run's charged-event counter after it settles (~12 s).

An image ad's row looks the same, with `imageText` in place of the transcript:

```json
{
  "status": "image_text_extracted",
  "charged": true,
  "advertiser": "HubSpot",
  "format": "SPONSORED_STATUS_UPDATE",
  "adId": "1478297483",
  "detailUrl": "https://www.linkedin.com/ad-library/detail/1478297483",
  "imageUrl": "https://media.licdn.com/dms/image/v2/%E2%80%A6",
  "imageText": {
    "headline": null,
    "body": null,
    "cta": null,
    "displayUrl": null,
    "rawText": "46% of marketers struggle with visual consistency. Get the free HubSpot & Canva Playbook HubSpot | Canva"
  },
  "chargeEvents": { "transcript": 1, "surchargeMinutes": 0 }
}
```

`advertiser` always comes from LinkedIn's own metadata, never from reading a logo in the picture.

Every field is always present (explicit `null` over silent omission). Non-delivered rows carry `status` + `statusReason` instead — `ocr_no_text_found` (a genuinely text-free creative, e.g. a plain stock photo), `image_expired` (most creative image links never expire, but a small share — 2 of 76 measured — are short-signed and can go stale in a chained dataset; re-run your scraper for fresh URLs, or search here so the link and the picture are read in the same run), `image_skipped` (you turned image reading off), `non_video_skipped` (document, carousel, article, message and text-only creatives pass through with their metadata), `no_audio_stream` (a genuinely silent video), `no_speech` (music-only audio), `no_onscreen_text`, `not_found`, `blocked_by_challenge`, `unavailable_expired`, `no_videos_found`, `search_unavailable`, `asr_unavailable` (the speech and text-reading service refused this actor's access mid-run — every ad left in the queue comes back as its own uncharged, retryable row instead of disappearing; re-run later), `skipped_budget` (the run reached your max charge before this ad's turn came at all — the row still carries the advertiser, format, ad ID and Ad Library link, and `retryable: true`, so you can raise the cap or pass the link in `videoUrls` next time; an ad already being transcribed or read when the cap was reached is delivered in full instead, uncharged; ads beyond your own `maxItems` are counted in the status line instead), `no_creative_found`, `input_error`, or `failed_*` (also listed in the run's `ERRORS` record). None of them are charged. `availability` and `impressions` are filled when LinkedIn shows them (ads shown in the EU) and `null` otherwise. The run's status line reconciles every ad it found: queued (with search results folded because they reuse one creative — one charge per creative), processed, left, and the cap that stopped it.

Every row carries `"isNew": true` for an ad your account had never answered before, `"isNew": false` for one it had, and `"firstSeenAt"`, the moment it was first answered (this run's clock for a new ad, the original sighting for an old one). An ad you already had comes back with `"repeat": true` and `"firstSeenRunId"` naming the run that first delivered it, `"charged": false`, and its `statusReason` saying so.

### Watch competitors on a schedule — pay only for new ads

**You never pay for the same ad twice.** Every run remembers the ads it answered for your account — in a key-value store called `linkedin-ads-watch-account` in your own Apify account — under every identity the ad has (its Ad Library ID, its creative's asset ID, its media URL). Run the same links, IDs, search or chained rows again and the ads you already have come back from the run that delivered them: `repeat: true`, `firstSeenRunId`, not fetched, not transcribed, not charged. The status line counts them (*2 of these you already had: handed back, not charged*) and `OUTPUT.repeats` holds the number. Delete that store to forget everything; entries older than 90 days are no longer treated as repeats.

Competitor research is rarely a one-off: the question is usually *what did they launch since last week?* A **watchlist** answers that with only the new ads.

1. Put the advertisers you follow in **Advertiser names** (or keywords, a market filter, a pasted search link, or a chained scraper run — any input mode works).
2. Give the run a **Watchlist name**, e.g. `acme-competitors`, and turn on **New ads only**.
3. Save the input as a **Task**, put the Task on an Apify **Schedule** (daily works well for active advertisers), and add an integration or a webhook on *run succeeded* — a Google Sheet, Slack, n8n, Make, or your own endpoint.

```json
{
  "accountOwners": ["HubSpot", "Salesforce"],
  "countries": ["US"],
  "watchlistId": "crm-competitors",
  "newAdsOnly": true
}
```

Each scheduled run then delivers **only the ads the list has not answered before**, each with `isNew: true` and its `firstSeenAt`. Ads already on the list are skipped before anything is fetched — not downloaded, not transcribed, not charged — and the run's status line says exactly what happened, for example: *watchlist "crm-competitors": 4 new ads this run, 61 already on the list were skipped (not fetched, not charged); 65 on the list now.* The same numbers sit in the run's `OUTPUT` record under `watchlist`, `newAds` and `skippedSeen`.

Some details worth knowing:

- **The list lives in your account**, in a named key-value store called `linkedin-ads-watch-<your name>` (one record, `SEEN`). Open it in *Storage → Key-value stores* to see every ad the actor has answered under that name, or delete the record to start the list over. This actor runs with limited permissions and touches nothing else in your account.
- **Only answered ads go on the list.** A delivered transcript counts, and so does a final uncharged verdict such as *no speech*. A failed download, a page that could not be fetched this minute, or an ad your cost cap left out is **not** remembered, so the next run tries it again.
- **Leave New ads only off** and every ad runs as usual — you still get `isNew` / `firstSeenAt` on each row, which is the way to build a full "what is running now" picture while still knowing what is fresh.
- **New ads only without a watchlist name** compares against your account's memory instead: ads already delivered to your account are skipped before anything is fetched. Only when neither a list nor the memory can be read does the run stop with one uncharged row, instead of charging you for ads you may already have. A named watchlist it cannot open stops the run the same way, before any spend.
- **If your account's memory cannot be read**, the run still runs: it delivers and charges as usual, and the status line and the charged rows say the repeat check was unavailable (`repeatCheck: "unavailable"`).
- One ad, one sighting, however it arrives — the same ad found by two searches, or pasted as a link and found by search, is one entry and one charge; a creative served at a different bitrate is recognised as the same ad.

### For AI agents & LLMs — copy this block

```
Actor:   steadyfetch/linkedin-ads-transcript-scraper  (Apify Store)
Does:    LinkedIn Ad Library video ads -> full transcript + opening hook +
         advertiser metadata. One JSON row per ad. No LinkedIn login or cookies.
Run:     POST https://api.apify.com/v2/acts/steadyfetch~linkedin-ads-transcript-scraper/run-sync-get-dataset-items?token=APIFY_TOKEN
Input    (any ONE target is enough — a market filter on its own counts as one):
  {
    "accountOwners": ["HubSpot"],          // advertiser names; the actor finds their video ads
    "keywords": ["crm software"],          // or search the Ad Library by topic
    "countries": ["US"],                   // market filter: 2-letter codes as the Ad Library lists
                                           // them (GB, not UK) or "ALL". One search per country.
    "dateOption": "last-30-days",          // last-30-days | current-month | current-year |
                                           // last-year | custom-date-range (+ startdate/enddate)
    "payer": "",                           // optional: the organisation that paid for the ads
    "impressionsMin": 0,                   // optional; EU-served ads only (LinkedIn publishes
    "impressionsMax": 0,                   // impression counts for EU ads only)
    "videoUrls": ["https://www.linkedin.com/ad-library/detail/1508450294",
                  "https://www.linkedin.com/ad-library/search?keyword=crm&countries=US"],
    "datasetId": "<dataset id of any LinkedIn Ad Library scraper run>",
    "watchlistId": "crm-competitors",      // optional: remember answered ads under this name (your own account)
    "newAdsOnly": false,                   // with a watchlistId: skip ads already on the list before any fetch
    "includeImageText": true,              // default ON: read image ads' copy (charged like a transcript)
    "includeOnScreenText": false,          // default OFF: read silent video ads' on-screen text
    "includeNonVideo": false,              // optional: uncharged rows for document/carousel/article/text ads
    "maxItems": 1000                       // optional cap — counts video AND image ads
  }
Nulls    every optional field above also accepts an explicit null, meaning "use the
         default" — so a template that renders unset fields as null (n8n, agent
         frameworks, chained workflows) runs exactly like one that omits them.
Output   one row per ad: status, charged, isNew, firstSeenAt, advertiser, headline, adText, ctaText, payingEntity,
         adId, detailUrl, format, availability, impressions, impressionsByCountry,
         hook3s, hookStartSeconds, transcript, language, durationSeconds, segments[],
         imageUrl, imageText{headline,body,cta,displayUrl,rawText}, onScreenText{...}
Pricing  pay per event, platform usage INCLUDED, no start fee:
  video-ad-transcript   $0.020 -> $0.008 per delivered transcript OR image-ad text extraction
                        (falls with your Apify plan tier; an image never adds a surcharge)
  long-video-surcharge  $0.005 per started minute past the first 3 minutes
  Charged only on delivery — creatives with no readable text, blocked pages and failures cost $0.
  Ads skipped by a watchlist (newAdsOnly) are never charged.
  ~4 in 5 LinkedIn ads are image ads, so a keyword run charges for ~10x more rows than video alone.
  Cap any run with maxTotalChargeUsd (minimum $0.05).
MCP:     pin this actor in any MCP client with https://mcp.apify.com?tools=steadyfetch/linkedin-ads-transcript-scraper
         (or ask Apify's MCP server for "linkedin ad transcripts").
```

### Why this LinkedIn Ad Library transcript scraper?

- **It reads the whole library, not just the 7% that is video.** The Ad Library has no format filter. This actor reads the format off each search result first, transcribes the video ads, and reads the on-image copy of the image ads — so a keyword search comes back with the ad copy of ~95% of what it saw instead of ~7%.
- **Browse a whole market, no keyword required.** Set a country and a date range and the actor walks that market's recent ads — the way you would use the Ad Library's own filters, except every ad comes back as text. Useful when you don't yet know which advertisers or phrases to look for. Filters work alongside advertisers and keywords too, and LinkedIn applies them itself, so a filtered search is a genuinely different result set, not a trimmed one.
- **Paste the link you already have.** Filtered the Ad Library by hand in your browser? Paste that `linkedin.com/ad-library/search?…` link into **Ad URLs, video URLs or search links** and the run inherits every filter in it.
- **Image ads cost no extra page loads.** The creative image URL is already in the search result, so reading image ads adds zero requests to LinkedIn.
- **Charged only on delivery.** Text either lands in your dataset or the row is not charged. Text-free stock photo → uncharged row that says so. Silent creative → uncharged row that says so. Blocked page → uncharged row that says so. No "it ran and charged me but returned nothing."
- **The run tells you what it saw.** The status line names every format the search returned, including the ones this actor does not read — nothing is dropped silently.
- **Old datasets still work.** LinkedIn ad video URLs are long-lived — unlike other ad platforms' CDN links, they don't expire in hours or days. A dataset you scraped weeks ago transcribes fine today.
- **No cookies, no login.** Runs entirely on LinkedIn's public Ad Library — nothing to paste, no account at risk.
- **Any video length.** Long-form B2B creatives transcribe fine — videos past 3 minutes add a small per-started-minute surcharge instead of failing.
- **Hook as data, and never blank.** `hook3s` is the ad's first 3 seconds of speech — the line media buyers study — as its own field, with full segment timestamps beside it. LinkedIn creatives that open on a branded title card or music are common, so when nothing is said in the first 3 seconds you get the first 3 seconds of speech from wherever speech actually begins, and **`hookStartSeconds` tells you which second that is** (`0` when the ad opens speaking). An ad with no speech at all leaves both `null` — this field is never an empty string, so "the ad opens silent" and "the ad opened talking, we lost it" can't be confused. Sort by `hookStartSeconds` to see which competitors make you wait for the pitch.
- **Works with any scraper's output.** The dataset input deep-scans your rows for LinkedIn ad video links and ad detail links — no field mapping, no glue code.
- **Ad metadata included.** Advertiser, headline, ad text, paying entity, run dates, and — where LinkedIn shows them (EU-targeted ads) — estimated impressions with the per-country split, on every row.

This actor may fail when the platform changes things — failed items are never charged.

### How to transcribe LinkedIn video ads (no code)

1. Type an advertiser name (e.g. `HubSpot`) into **Advertiser names** — or a topic into **Keywords**. Or leave both empty and set **Countries** (e.g. `US`) and a **Date range** under *Market filters* to browse that market's recent ads instead.
2. Click **Start**. The actor searches the Ad Library, picks out the video ads, and transcribes each one.
3. Each ad comes back as one JSON row: a transcript + hook for video ads, the on-image copy for image ads, plus advertiser metadata.
4. Export as JSON/CSV, or read it via API.

Already scraping the Ad Library? Paste your run's **dataset ID** (or use Apify's *Connect Actor* integration) and this actor transcribes the video ads in it.

### How much does it cost to transcribe LinkedIn ads?

**One result = one ad's text payload** — a video ad's transcript + hook + segments, or an image ad's on-image copy. Both cost the same event. The first 3 minutes of each video are included; longer videos add a small per-started-minute surcharge, and an image ad never does. No start fee, no subscription, no third-party API key needed.

| Job | Approx. cost |
|---|---|
| 50 competitor ads (video or image) | ≈ $1.00 |
| 500 ads (a serious creative teardown) | ≈ $10 |
| Apify free plan ($5 credit) | ≈ **250 ad creatives** |

**Sizing a keyword run:** roughly 4 in 5 LinkedIn ads are image ads and about 3 in 4 of those carry
readable copy, so one page of 24 search results yields on the order of 15 charged rows. Set
`maxItems` to the number of ads you actually want, and cap the run with **max total charge** —
both stop the run honestly with a "what's left" message rather than overspending.

Platform usage (compute + transfer) is **included in the event price** — the price you see is the whole price. Searching, unreadable creatives, and failed fetches carry no result fee.

**How often should you re-run this?** Once an ad has been delivered to your account, this actor remembers it for 90 days: re-running the same advertisers, keywords or ad links hands those ads straight back from the run that first answered them, with **no result fee** — so a re-run only ever charges for ads it has not answered for you before. Re-running the same input an hour later therefore returns the same rows and adds no result fee. LinkedIn's Ad Library changes on the scale of days, not minutes, so a **daily or weekly** schedule is what actually surfaces new creatives. Turn on *New ads only* (`newAdsOnly`) if you would rather have the ads you already hold skipped as a named count than handed back as rows.

### Works with — LinkedIn Ad Library scrapers you can chain from

The dataset input reads the **values** in each row (any `dms.licdn.com` video link or `ad-library/detail/…` link), not fixed field names — so it works with any Ad Library scraper, including:

| Scraper | Chain by |
|---|---|
| [silva95gustavo/linkedin-ad-library-scraper](https://apify.com/silva95gustavo/linkedin-ad-library-scraper) | dataset ID or pasted rows (`videoUrl` / detail links) |
| [dz\_omar/linkedin-ads-scraper](https://apify.com/dz_omar/linkedin-ads-scraper) | dataset ID or pasted rows (`detailPageUrl`, `detail.videoUrls`) |
| [memo23/linkedin-ads-scraper](https://apify.com/memo23/linkedin-ads-scraper) | dataset ID or pasted rows (`adLibraryUrl` + `format: "VIDEO"`) |
| [ivanvs/linkedin-ad-library-scraper](https://apify.com/ivanvs/linkedin-ad-library-scraper) | dataset ID or pasted rows (`url` detail links) |
| [automation-lab/linkedin-ad-library-scraper](https://apify.com/automation-lab/linkedin-ad-library-scraper) | dataset ID or pasted rows (`detailUrl`, `mediaUrl`) |

**How to chain:** run the scraper → copy the run's **dataset ID** into this actor's *Dataset ID* field (or use Apify's *Connect Actor* integration) → Start. Video rows are transcribed; image rows have their on-image copy read (turn *Read the text on image ads* off to pass them through uncharged instead); rows with no recognizable creative come back as uncharged `no_creative_found`. One caveat for chained rows: most creative **image** links never expire, but a small share are short-signed and can go stale in an older dataset — those rows come back as uncharged `image_expired`, and searching by advertiser/keyword here avoids the problem entirely. You can also skip the other scraper entirely — this actor searches the Ad Library itself.

### Use it via API, MCP, and integrations

- **API**: standard Apify run API — `POST .../acts/steadyfetch~linkedin-ads-transcript-scraper/runs` with `{ "accountOwners": ["HubSpot"] }`. Python/Node clients work as with any actor.
- **MCP**: one link pins this actor in Claude, Cursor, or any MCP client — `https://mcp.apify.com?tools=steadyfetch/linkedin-ads-transcript-scraper` — or ask Apify's MCP server for "linkedin ad transcripts".
- **n8n**: a free ready-made template, no community nodes needed — **LinkedIn Ad Library transcripts** (one advertiser → hooks & transcripts → spreadsheet-shaped rows, 3-minute setup), listed on our profile website (the link on our Apify profile page).
- **Make / Zapier**: call it as a regular Apify actor step right after your scraper node, or run it standalone with advertiser names.
- **Schedules + webhooks**: save any input as a Task, schedule it, and use the Task's *Integrations* tab to push each run's new rows wherever they need to go.

### FAQ

**How does the watchlist know what I have already seen?** Every ad the actor answers under a watchlist name is written to a key-value store in your own account (`linkedin-ads-watch-<name>`), keyed by the ad's Ad Library ID (or the creative's asset ID when there is no ID). The next run with that name reads the list first. Delete the store's `SEEN` record to start over. Nothing is stored anywhere else.

**Does a watchlist run charge for the ads it skips?** No. With **New ads only** on, an ad already on the list is dropped before anything is fetched; the status line and `OUTPUT.skippedSeen` count them, and no row and no charge is produced for them.

**Where do the ads come from?** LinkedIn's own Ad Library — the public transparency surface LinkedIn publishes so anyone can see who runs which ads. No login or cookies are used, ever.

**Can I run it with no keyword — just a country?** Yes. Set **Countries** and a **Date range** under *Market filters* and leave the advertiser and keyword fields empty: the actor browses that market's recent ads and returns them as text. LinkedIn applies the filters on its side, so this is a genuinely different result set from an unfiltered search. Leaving *everything* empty is still an error — the run needs at least one advertiser, keyword, market filter, link or dataset.

**Why does each country cost a separate search?** The Ad Library reads only the first country you give it, so listing three countries would silently answer for one. The actor runs one search per country instead — they all count against **Max ads to process** and your max charge, exactly like extra keywords do.

**The impressions filter returns nothing.** LinkedIn only publishes impression counts for ads shown in the EU, so filtering on impressions also limits results to EU-served ads. On a US-only market it legitimately comes back empty — the row says so and nothing is charged.

**Why did some ads come back `non_video_skipped`?** Document, carousel, article, message and text-only creatives are not read — there is no video to transcribe and no single ad image to read. They pass through uncharged with their advertiser/CTA metadata (turn on *Include the remaining formats* to get one uncharged metadata row for each one found in search).

**How accurate is the image-ad text?** It is read by a vision model from the ad's own creative, verbatim and untranslated, and delivered as `imageText.rawText` with the headline / body / CTA split out where the model can tell them apart. Non-English copy works. If nothing readable comes back, the row is `ocr_no_text_found` and uncharged — a plain product photo with no copy on it is never sold to you as a result.

**Can I get the on-screen text of silent video ads?** Yes — turn on *Read on-screen text on silent video ads*. About half of LinkedIn's video ads are silent or music-only, and most of them carry their whole message as on-screen type. With the switch on, those ads' frames are read and delivered as `onScreenText`, charged like a transcript; silent ads with no real copy (plain b-roll) stay uncharged. With the switch off — the default — a silent ad is never charged, exactly as before.

**A search returned `no_videos_found`?** The advertiser, keyword or market had no video ads and no image ads this actor could read — only document, carousel, article or message creatives, or nothing at all. The row tells you how many creatives were seen and of which formats, so you can widen the query. When you search several advertisers, keywords or countries at once, each search that turns up nothing readable gets its own uncharged row naming it — filters included, so a misspelled advertiser never disappears silently. Nothing is charged.

**Silent video ads?** Some B2B creatives ship with no audio track at all (text-overlay videos). We detect that and return an honest, uncharged `no_audio_stream` row instead of an empty transcript — or, with *Read on-screen text on silent video ads* turned on, we read the type off the video's own frames.

**Music-only ads?** Speech-recognition models hallucinate filler on music. We detect that and return an honest, uncharged `no_speech` row instead of selling you a fake transcript.

**What is `blocked_by_challenge` / `search_unavailable`?** LinkedIn occasionally serves a verification page instead of an ad page, or the Ad Library search returns nothing for a while. The actor retries, and if it still can't get through you get an uncharged row that says so — re-running a few minutes later usually picks those ads up.

**Typos in bare ad IDs?** A mistyped numeric ad ID is indistinguishable from a real one, so it surfaces as an honest, uncharged `not_found` row (LinkedIn answers "can't load this ad") rather than an input error.

**Languages?** Whisper-class multilingual ASR — German, French, Spanish, Arabic, Japanese, and ~90 more transcribe out of the box; the detected `language` ships on every row.

**Is it legal to transcribe LinkedIn ads?** The Ad Library is public by design (ad transparency, required by regulations like the EU DSA). This actor reads only that public surface, logged out. As always, how you use the data is on you.

**Is this an official LinkedIn product?** No. This is an independent actor from steadyfetch — unofficial, and not affiliated with, endorsed by, or sponsored by LinkedIn Corporation or Microsoft. "LinkedIn" is a trademark of its respective owner, used here only to name the public Ad Library surface this actor reads.

### Steadyfetch ad-intelligence suite

One actor per ad surface, one job each, the same contract everywhere: **all-inclusive pay-per-event,
no start fee, charged only on delivery** — and the same row shape, so a workflow built for one platform
ports to the next.

**Ad creatives → transcripts & on-creative text**

| Ad surface | Actor | Free n8n template |
|---|---|---|
| Facebook & Instagram Ad Library | [Facebook Ads Transcript Scraper — Ad Library Video & Image Ads](https://apify.com/steadyfetch/facebook-ads-transcript-scraper) | Facebook ad transcripts · Competitor ad teardown |
| Google Ads Transparency Center | [Google Ads Video Transcript Scraper — Ads Transparency Center](https://apify.com/steadyfetch/google-ads-video-transcript-scraper) | Google Ads video transcripts |
| TikTok Creative Center Top Ads | [TikTok Ads Transcript Scraper — Creative Center Top Ads & Hooks](https://apify.com/steadyfetch/tiktok-ads-transcript-scraper) | TikTok Top Ads transcripts |
| **LinkedIn Ad Library** | **this actor** | LinkedIn Ad Library transcripts |
| Instagram Reels (organic posts) | [Instagram Reel Transcript Scraper — Reels to Text with 3s Hooks](https://apify.com/steadyfetch/instagram-reel-transcript-scraper) | Instagram Reel transcripts |

**Ad creatives → text (image & text ads, read by OCR)**

| Ad surface | Actor |
|---|---|
| Google Ads Transparency Center | [Google Ads Transparency Scraper — Ad Copy, CTAs & Image OCR](https://apify.com/steadyfetch/google-ads-creative-text-scraper) |

Free n8n workflow templates for this actor are listed on our profile website (link on our Apify profile).

**The rest of the steadyfetch shelf** — same contract everywhere: all-inclusive pay per event,
no start fee, charged only on delivery.

| Family | Actors |
|---|---|
| **Ad creative intelligence** | [Facebook](https://apify.com/steadyfetch/facebook-ads-transcript-scraper) · [Google Ads video](https://apify.com/steadyfetch/google-ads-video-transcript-scraper) · [TikTok](https://apify.com/steadyfetch/tiktok-ads-transcript-scraper) · [LinkedIn](https://apify.com/steadyfetch/linkedin-ads-transcript-scraper) · [Google Ads text & OCR](https://apify.com/steadyfetch/google-ads-creative-text-scraper) |
| Trends & keywords | [Google Trends](https://apify.com/steadyfetch/google-trends-scraper) · [Trends Now](https://apify.com/steadyfetch/google-trends-now-scraper) · [Breakout keywords](https://apify.com/steadyfetch/breakout-keywords-scraper) · [Autocomplete keywords](https://apify.com/steadyfetch/google-keyword-suggest-scraper) · [Keyword volume & CPC](https://apify.com/steadyfetch/keyword-search-volume-scraper) · [Social trends](https://apify.com/steadyfetch/social-trends-scraper) |
| YouTube transcripts | [YouTube videos](https://apify.com/steadyfetch/youtube-transcript-scraper) · [YouTube channels](https://apify.com/steadyfetch/youtube-channel-transcripts) |
| Instagram | [Reel transcripts](https://apify.com/steadyfetch/instagram-reel-transcript-scraper) · [Profile posts](https://apify.com/steadyfetch/instagram-profile-posts) |
| Jobs | [Indeed](https://apify.com/steadyfetch/indeed-jobs-scraper) · [Career sites by domain](https://apify.com/steadyfetch/company-jobs-by-domain) · [Glassdoor](https://apify.com/steadyfetch/glassdoor-jobs-scraper) · [Multi-board](https://apify.com/steadyfetch/multi-job-board-scraper) · [Google Jobs](https://apify.com/steadyfetch/google-jobs-scraper) |
| Amazon | [Products](https://apify.com/steadyfetch/amazon-product-scraper) · [Search](https://apify.com/steadyfetch/amazon-search-scraper) · [Bestsellers](https://apify.com/steadyfetch/amazon-bestsellers-scraper) · [Sellers](https://apify.com/steadyfetch/amazon-seller-scraper) |
| Any media file | [Speech to Text · any link or file](https://apify.com/steadyfetch/media-transcriber) |

### Feedback & support

Found an issue? Open it on the **Issues tab** — we usually reply within a couple of hours, always within a day. Feature requests welcome. And if this actor earned its keep, a rating helps other buyers find it, and saving it keeps it one click away.

# Actor input Schema

## `accountOwners` (type: `array`):

Company names as they appear in the Ad Library (e.g. "Salesforce", "HubSpot"). Each is searched with the Ad Library's own advertiser search. Leave everything empty and click Start and the run is a 3-creative sample (a browse of the United States, last 30 days, charged like any run) — the fastest way to see real output.

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

Free-text Ad Library searches (e.g. "crm software", "recruiting"). Finds video ads across many advertisers in a niche.

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

Countries as the Ad Library lists them — the code or the country name both work, so US, USA and United States are one market, and UK reaches GB. "ALL" searches every country. LinkedIn searches one country at a time, so each one you add is its own search (they add up against "Max ads to process"). A country the Ad Library does not offer is refused, uncharged, and the row says which of the three things went wrong (a real market it does not offer, a region-wide ask, or a value nothing could place).

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

When the ads first ran. The value or the name of the window both work — last-30-days and "Last 30 days" are the same range. LinkedIn only serves ads created in roughly the last 12 months, whichever option you pick. Choose "custom-date-range" to use the two date fields below. A window this actor cannot read is never swapped for another period — it scopes every search the run makes, so the run stops with uncharged rows naming this field, the accepted values and every search it did not run.

## `startdate` (type: `string`):

YYYY-MM-DD, e.g. 2026-08-01. Used with "custom-date-range" (setting both dates selects it for you). LinkedIn rejects dates more than about a year old.

## `enddate` (type: `string`):

YYYY-MM-DD, e.g. 2026-08-30. Must be on or after the start date.

## `payer` (type: `string`):

The organisation that paid for the ads, as the Ad Library's "Payer" filter spells it. Often differs from the advertiser on the creative (an agency, or a regional entity).

## `impressionsMin` (type: `integer`):

Whole number of impressions, e.g. 50000. LinkedIn only publishes impression counts for ads shown in the EU, so this filter also limits results to EU-served ads — on a US-only market it returns nothing.

## `impressionsMax` (type: `integer`):

Whole number of impressions, higher than the minimum. EU-served ads only, same as the minimum above.

## `videoUrls` (type: `array`):

Ad Library detail URLs (linkedin.com/ad-library/detail/…), numeric ad IDs, direct video URLs (dms.licdn.com/playlist/vid/…) from any LinkedIn Ad Library scraper result — or a whole Ad Library search link (linkedin.com/ad-library/search?…), which is run as a search with the filters already in it.

## `datasetId` (type: `string`):

The default dataset ID of a finished LinkedIn Ad Library scraper run. With Apify's 'Connect actor' integration this is filled automatically — the cleanest way to chain.

## `datasetItems` (type: `array`):

Paste rows from your Ad Library scraper run. The actor deep-scans each row for video URLs, ad detail links, and metadata — works with any scraper's output shape. Non-video rows come back as uncharged metadata rows. Leave it empty to search directly or use the other inputs instead.

## `watchlistId` (type: `string`):

A short name for the list, e.g. `acme-competitors` (letters, numbers and hyphens). The list lives in a named key-value store in your account called `linkedin-ads-watch-<name>`, so you can inspect or clear it any time. Leave empty for a one-off run — your account's memory of delivered ads still applies.

## `newAdsOnly` (type: `boolean`):

OFF (default): every ad runs as usual; an ad your account already has comes back from the earlier run, not charged (`repeat: true`), and every row carries `isNew` / `firstSeenAt`. ON: ads already on your watchlist — or, with no list name, already delivered to your account — are skipped before anything is fetched: not fetched, not charged, and the run's status line and OUTPUT say how many. Only an ad the actor actually answered counts (a delivered transcript, image text or on-screen text, or a final uncharged verdict such as no speech); an ad it could not answer — a blocked page, a failed fetch, an ad your cost cap left out — does not, so the next run tries it again.

## `includeImageText` (type: `boolean`):

ON (default): image ads get their on-image copy read and delivered as `imageText` — each delivered extraction is charged like a video transcript (same event, same price; never a long-video surcharge). Image ads with no readable text are always uncharged. Most LinkedIn ads are image ads, so a keyword run charges for many more rows with this on than a video-only run did — set `maxItems` and the run's max total charge to match. OFF: image ads pass through uncharged with their metadata, as before.

## `includeOnScreenText` (type: `boolean`):

OFF (default): a video ad with no speech is never charged. ON: silent and music-only video ads have their on-screen text read from the video's own frames and delivered as `onScreenText`, charged like a transcript. Ads with no readable on-screen text stay uncharged either way. About half of LinkedIn's video ads are silent.

## `includeNonVideo` (type: `boolean`):

When searching by advertiser/keyword, also output one uncharged metadata row (advertiser, format, ad link) for each creative this actor does not read — document, carousel, article, message and text-only ads. Off by default.

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

Safety cap on how many ads to process in one run (every attempted video ad and image ad counts toward it, including uncharged misses; the formats above never do). Long videos: the first 3 minutes of every video are included in the transcript price; each started minute beyond that is charged as a "Long-video minute (surcharge)" at $0.005 — only when the transcript is delivered, never on image ads or on-screen text.

## Actor input object example

```json
{
  "newAdsOnly": false,
  "includeImageText": true,
  "includeOnScreenText": false,
  "includeNonVideo": false,
  "maxItems": 1000
}
```

# Actor output Schema

## `transcripts` (type: `string`):

One row per ad: transcript or on-image text, opening hook, advertiser, headline, format, and a status (transcribed / image\_text\_extracted / non\_video\_skipped / no\_speech / not\_found / …). Only delivered rows are charged — 'transcribed' and 'image\_text\_extracted' at the same price; everything else is uncharged, and an ad already being transcribed or read when your max charge is reached still delivers in full, uncharged. hook3s is the first 3 seconds of speech; if the ad says nothing in its first 3 seconds, it is the first 3 seconds of speech from wherever speech begins, and hookStartSeconds says which second that is (0 when the ad opens speaking). An ad with no speech at all has both as null — never a blank string. With a watchlist name set, every row also carries isNew (the list had not answered this ad before) and firstSeenAt (when it first did); with New ads only on, ads already on the list are skipped before anything is fetched and never charged. Ads your account already had come back from the run that delivered them, `repeat: true` with `firstSeenRunId`, not charged.

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

Delivered count, uncharged metadata rows, uncharged misses, failures, and the honest status message.

## `errors` (type: `string`):

Present only when fetches/ASR failed after all retries: ad ID and reason.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/linkedin-ads-transcript-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("steadyfetch/linkedin-ads-transcript-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 '{}' |
apify call steadyfetch/linkedin-ads-transcript-scraper --silent --output-dataset

```

## MCP server setup

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