# Facebook Ads Transcript Scraper — Ad Library Video & Image Ads (`steadyfetch/facebook-ads-transcript-scraper`) Actor

Meta ads transcript in bulk: Facebook, Instagram and Meta Ad Library video ads with the first-3s hook, plus image-ad text. Search by keyword or advertiser — no ad IDs needed, or paste ad links or chain a scraper. No start fee; expired links, text-free images and music-only ads are never charged.

- **URL**: https://apify.com/steadyfetch/facebook-ads-transcript-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** Social media, Videos, Lead generation
- **Features:** Limited permissions, Multi-tenant, Server interface
- **Stats:** 220 total users, 79 monthly users, 100.0% runs succeeded, 2 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.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?

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

## Facebook Ads Transcript Scraper — Ad Library Video & Image Ads

**Type a keyword or a competitor's page name and get their Facebook and Instagram ads back as text.** Every video ad comes back with its **full transcript** and the **opening hook**; every image ad with its **on-image copy** (headline, body, CTA); both with the ad's CTA, advertiser and ad ID, one JSON row per ad, any video length, in bulk. **From $20.00 per 1,000 ad creatives** on the Business plan ($30.00 on the free plan), platform usage included, no start fee. You are charged only when an ad's content lands in your dataset: searches that match nothing, expired links, music-only videos and text-free images cost $0.

**Coming back for new ads?** Advertisers launch and pause ads every day, so the same keyword or competitor page shows a different set of running ads from one week to the next. To follow them, save your input as a Task with a watchlist — e.g. `{ "searchQueries": ["Acme Fitness"], "watchlistId": "acme-competitors", "newAdsOnly": true }` — and put that Task on an Apify **Schedule**: each run skips the ads your account already has before downloading them, so it transcribes and charges only what is new. Even without a watchlist, an ad already delivered to your account is handed back from that earlier run and never charged a second time, whatever mode found it. Daily suits an advertiser who launches often; weekly is enough for a category keyword ([how the watchlist works](#watch-competitors-on-a-schedule--pay-only-for-new-ads)).

**See a real run before you spend anything:** [sample dataset](https://api.apify.com/v2/datasets/79hMkGYDnmPi3z9fd/items?clean=true\&format=json) — real ads from one verified run: three transcripts charged, one in Spanish, and one expired-link ad delivered uncharged. Unedited.

**Using an AI agent?** Pin this actor in Apify's MCP server with one link: `https://mcp.apify.com?tools=steadyfetch/facebook-ads-transcript-scraper`

- **Actor id:** `steadyfetch/facebook-ads-transcript-scraper`
- **Input:** `{ "searchQueries": ["fitness app"] }` — the one field you have to set.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `0.50` — the run stops when it reaches it. A run your cap stops says on its run page what the cap bought and the figure to set for the ads left.

**Just want to see it work?** Click **Start** with nothing set and the run is a 3-ad sample — currently running US video ads matching "fitness app", transcribed and charged like any run (about $0.09 on the free plan, $0.06 on Business, plus the Ad fee below). The sample always runs live against the Ad Library, so pressing Start again looks those ads up again and charges for them again — a few cents. Set only the settings — a country, an ad format, a cap — and no keyword, link or dataset, and that same sample runs **under your settings**, with one uncharged note row saying which ones it used; a limit you set is a ceiling, so the sample keeps its own small size. Already have specific ads? Paste Ad Library links, or chain any Ad Library scraper's run by dataset ID. Watching one advertiser? Paste the address bar from their Ad Library page and the run searches that page.

![The input form: search keywords, country, Ad Library links, and the watchlist fields](https://api.apify.com/v2/key-value-stores/wciMDNpNgBh87mGUp/records/facebook-ads-transcript-scraper--input-form-watchlist.png)

| You give it | You get back |
|---|---|
| **A keyword or advertiser name** + a country — nothing else needed | `transcript` — full speech-to-text, any length (video ads; first 3 minutes included, then a small per-minute surcharge) |
| …or **Ad Library links** (`facebook.com/ads/library/?id=…`) | `hook3s` — the ad's opening line, plus `hookStartSeconds` |
| …or ad video URLs (`video_hd_url` from any scraper) | `imageText` — headline, body, CTA and full visible text (image ads) |
| …or your scraper run's **dataset ID** (one-click chaining) | `ctaText`, `pageName`, `adArchiveId`, `searchQuery`, `language`, `durationSeconds`, timestamped `segments` |
| …or pasted dataset rows (any scraper's shape) | per-row `status` — so an uncharged row always says why it was uncharged |
| …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 |
| …a keyword, an **Ad Library link** or an advertiser's page link | `adLibrary` — the rest of that ad's Ad Library row: its flight dates, whether it is still running, the platforms, the landing page, and the advertiser's page (listed in full below) |

### What the output looks like

One JSON row per ad. Delivered rows carry the transcript and `charged: true`; anything that could not be delivered carries a `status` saying why, and `charged: false`. One case delivers uncharged: if your maximum cost is reached while an ad is already being transcribed, 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 | hook3s | hookStartSeconds | transcript | language | durationSeconds |
|---|---|---|---|---|---|---|
| `unavailable_expired` | `false` | *null* | *null* | *null* | *null* | *null* |
| `transcribed` | `true` | Hice este desafío de Pelatis en la pared de 28 días para mi boda y ahora lo haré | 0 | Hice este desafío de Pelatis en la pared de 28 días para mi boda y ahora lo haré nuevamente para mis vacaciones. Sigo estos entrenamientos directamente desde mi… | Spanish | 40.00 |
| `transcribed` | `true` | Your sister's on the phone. | 0 | Your sister's on the phone. So, today… Ha ha ha, time to prank like a pro. Drop a giant cockroach or a crawling spider on your friend's face… | English | 36.71 |
| `transcribed` | `true` | Look, women are always right. My girlfriend told me to get the Native Facial Hyd | 0 | Look, women are always right. My girlfriend told me to get the Native Facial Hydrating Cleanser. Boy, I tell you, my face never felt this soft in my life… | English | 32.81 |

The first row is a video URL that had already expired: it comes back with the reason and is **not charged**. This example run was fed direct video URLs, so its `pageName`, `adArchiveId` and `ctaText` columns are `null` — search by keyword or paste `facebook.com/ads/library/?id=…` links instead and the actor fills those in for you.

![A run's output table in the Apify console: status, charged, new?, first seen, advertiser, opening hook, CTA, transcript](https://api.apify.com/v2/key-value-stores/wciMDNpNgBh87mGUp/records/facebook-ads-transcript-scraper--output-table-watchlist.png)

Full row, as JSON — a real delivered row from a pasted Ad Library link, which is why `pageName`, `adArchiveId` and `ctaText` are filled in here (its `adLibrary` object is listed separately below):

```json
{
  "status": "transcribed",
  "charged": true,
  "pageName": "Shopify",
  "adArchiveId": "1265622684930223",
  "hook3s": "Shopify made it so much easier to see what was going on whilst being on the go.",
  "hookStartSeconds": 0,
  "ctaText": "Sign up",
  "ctaType": "SIGN_UP",
  "transcript": "Shopify made it so much easier to see what was going on whilst being on the go. It fits into my life as a busy working mum so well. I'm Nicola, my business is Hutch Cassidy, painted by me, powered by Shopify.",
  "language": "English",
  "durationSeconds": 15.04,
  "segments": [{ "start": 0, "end": 5.94, "text": " Shopify made it so much easier to see what was going on whilst being on the go." }],
  "chargeEvents": { "transcript": 1, "surchargeMinutes": 0 }
}
```

**[Browse the full live example dataset →](https://api.apify.com/v2/datasets/79hMkGYDnmPi3z9fd/items?clean=true\&format=json)** — real ads from a real run: three transcripts (one in Spanish) and one expired-URL row delivered honest and uncharged.

Every row carries `"isNew": true` for an ad your account had never answered before, `"isNew": false` for one it had, and `"firstSeenAt": "2026-09-01T18:42:10.113Z"`, 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.

**Every ad found by keyword, by an advertiser's page link, or resolved from an Ad Library link also comes back with `adLibrary`** — the rest of that ad's Ad Library row, beside the transcript, so you do not need a second scraper for it. The flight: `startDate`, `endDate`, `isActive`, `totalActiveTimeSeconds`. Where it runs: `publisherPlatforms` (the platforms Meta lists the ad on) and `countries`. The creative as the Ad Library describes it: `displayFormat`, `title`, `caption`, `linkUrl` and `linkDescription` — the page the ad sends people to, and its headline copy. The advertiser: `pageId`, `pageProfileUri`, `pageCategories`, `pageLikeCount`, `pageIsDeleted`, `byline`. The collation: `collationId` and `collationCount`, how many ad IDs Meta lists this one creative under. How Meta files it: `categories`, `gatedType`, `containsSensitiveContent`, `containsDigitalCreatedMedia`, `disclaimerLabel`. And the EU transparency bands where Meta publishes them: `currency`, `spend`, `impressionsText`, `reachEstimate`. It rides on every row the run writes for such an ad, delivered or not. An ad you supplied as a direct video URL or a chained scraper row has `"adLibrary": null` — we did not look it up, rather than the Ad Library having said nothing about it.

Image ads deliver as `image_text_extracted` rows: the same metadata plus `imageText` — `{ headline, body, cta, advertiser, displayUrl, rawText, truncated }`, read straight off the creative — and the ad's `imageUrl`. `truncated` is `false` on almost every ad; on one carrying more copy than a single read returns it is `true`, the text ships cut short at about 3,500 characters, and the row's note says so. A delivered image extraction is charged exactly like a video transcript (same price, and never a long-video surcharge). Prefer the old pass-through? Set **Extract image-ad text** off (`includeImageText: false`) and image ads ship uncharged with metadata only, as before.

Every field is always present (explicit `null` over silent omission). Non-delivered rows carry `status` + `statusReason` instead — `unavailable_expired` / `image_expired`, `no_audio` (music-only creative, or a video file with no audio track — the row itself tells you how to read that ad's on-screen text instead), `skipped_budget` (your maximum cost was reached before this ad was processed at all — it still ships as an uncharged row with everything already known about it, so the dataset always holds one row per ad you asked for; raise the cap and re-run those rows; an ad that was already being transcribed when the cap was reached is delivered in full instead, uncharged), `skipped_deadline` (the run's remaining time was too short to fetch and transcribe this ad — the same uncharged row, saying how much time was left, how much the ad needs and the run timeout to set; raise it, or split the batch, and re-run), `ocr_no_text_found` (an image ad with no readable text — a plain photo — ships uncharged), `ocr_unreliable` (our reader could not return this ad's text — either it could not be read reliably, or the ad carries more copy than one read returns; the row's note says which, and nothing is charged either way), `ocr_service_unavailable` (the text-reading service could not process the image right now; re-run), `asr_unavailable` (the transcription service refused this actor's access partway through the run, so this ad was never started — a problem on our side; marked `retryable`, and a re-run picks it up), `image_skipped` (image extraction turned off), `image_unavailable`, `no_onscreen_text` (a silent ad with no readable text on screen either, when on-screen reading is on), `video_too_large` / `image_too_large` (the creative is bigger than this actor will download, or bigger than our text reader takes in one request — a video is one attempt, then an uncharged row; an image is re-sent once at a smaller size, and only an image still too big after that ships uncharged), `no_creative_found`, `unavailable_removed`, `resolve_blocked`, `search_no_results` (a keyword no ad matched right now, or one whose every ad this run had already collected under an earlier keyword — the row says which of the two it was and what to widen), `search_cut` (the search found ads and one of this actor's own limits stopped it before your "Max ads per keyword" was filled — the row names that limit and what to change; nothing is charged), `search_blocked` (Facebook declined that search this minute; marked `retryable`, re-run it), `input_error` (something in the input could not be used — the row says what and how to fix it), `sample_note` (the run set settings but no keyword, link or dataset, so the sample ran under those settings — the row names them, uncharged), `input_note` (the run saying what it read from your input, or why it could not run the check for ads you already have — an advertiser's Ad Library page link read as a search of that page, a field name from another one of our actors accepted here, or a scoped API token that cannot open or cannot update your account's memory; nothing was wrong with the input and the note is uncharged), or `failed_*` (also listed in the run's `ERRORS` record) — a `failed_processing` row that simply ran out of time is marked `retryable`, and re-running it with more run memory (which also buys more CPU) is what long videos need. None of them are charged.

**Dynamic product ads** (the ones Facebook serves as a `{{product.name}}`-style template) ship `adText` as the wording of the creative variant that was actually transcribed. In the rare case no variant copy exists, `adText` keeps the raw placeholder and the row carries `adTextIsTemplate: true`, so a placeholder is never mistaken for broken data.

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

**You never pay for the same ad twice** (the default sample aside — see the end of this paragraph). Every run remembers the ads it answered for your account — in a key-value store called `fb-ads-watch-account` in your own Apify account — under every identity the ad has (its Ad Library ID, its video file, its image file). The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission (or Actor runs set to Full access); without it the run says so on its status line and cannot skip what you already have. Run the same video URLs, links, search or chained rows again, in any mode, and the ads you already have come back from the run that delivered them: `repeat: true`, `firstSeenRunId`, not downloaded, not transcribed, not charged. The status line counts them (*14 of these you already had: handed back, not charged*, or its short form on a long summary) and `OUTPUT.repeats` holds the number. Delete that store to forget everything; entries older than 90 days are no longer treated as repeats. The one exception is the default sample — a Start with nothing set, or with only settings set: it always runs live, so it is neither read from nor written to that memory, and a second bare Start pays for its three ads again. Every run that names its own keywords, links, dataset or video URLs is covered exactly as above.

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 your keywords or advertiser names in **Search keywords** (or paste Ad Library links, or chain a 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
{
  "searchQueries": ["Acme Fitness", "Rival Gym App"],
  "country": "US",
  "mediaType": "all",
  "watchlistId": "acme-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 downloaded — not fetched, not transcribed, not charged — and the run's status line says exactly what happened, for example: *watchlist "acme-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 `fb-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, image text or on-screen text counts, and so does a final uncharged verdict such as *no speech*. A failed download, a link Facebook declined to resolve 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 download. 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. When the reason is the token's own scope — a restricted-access token with no key-value store permission — the row says that, and which permission to grant, instead of asking you to re-run. 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"`), so you know a repeat may have been charged that once. The most common cause is a scoped API token: give it key-value store Read, Write and Create permission (Write is what lets the run remember what it delivered; without it every run pays again) — or set its Actor runs to Full access — under Settings → API & Integrations, or start the run from the console. A run that hits this ships one uncharged note row, first in the results, saying exactly that, and a run whose token can read the memory but not update it says so on the run page and in its own uncharged note row.
- One ad, one sighting, however it arrives — the same ad found by two keywords, or pasted as a link and found by search, is one entry and one charge.
- **One creative, one charge, even under two ad IDs.** Meta lists the same video under separate ad IDs when an advertiser runs it more than once, and both can come back in one search. You still get a row for every ad ID you asked about, each with the transcript in full — but the video is transcribed once and charged once, and the second row says which ad it repeats. The status line counts them (*2 of these run the same creative as another ad in this run: delivered in full, charged once*, or its short form on a long summary).

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

```
Actor:   steadyfetch/facebook-ads-transcript-scraper  (Apify Store)
Does:    Search Facebook & Instagram Ad Library ads by keyword or advertiser (or
         take ad links / a chained dataset) -> every ad's content. Video ads:
         full transcript + opening hook, any length. Image ads: the
         on-image text (headline/body/CTA/rawText). Plus CTA + ad metadata.
         One JSON row per ad. No Facebook login or cookies. Optional watchlist:
         re-run the same advertisers and receive only ads not seen before.
Run:     POST https://api.apify.com/v2/acts/steadyfetch~facebook-ads-transcript-scraper/run-sync-get-dataset-items?token=APIFY_TOKEN
Input    (any ONE of the target fields is enough):
  {
    "searchQueries": ["fitness app"],
                                  // keywords or advertiser page names — the cold start: no IDs needed
    "country": "US",              // two-letter code; which country's Ad Library to search
    "mediaType": "video",         // "video" (default) or "all" (also returns image ads)
    "activeStatus": "active",     // "active" (default, running now) or "all"
    "searchMaxAds": 25,           // how many ads each keyword may return; up to 500 — ask for more and the run continues at 500
    "adLibraryUrls": ["https://www.facebook.com/ads/library/?id=<ad id>"],
                                  // ad links or bare ad IDs (the actor finds the video), an advertiser's page link or a search link (read as that search)
    "videoUrls": ["https://video-xxx.xx.fbcdn.net/..."],
                                  // video_hd_url / video_sd_url from any Ad Library scraper
    "datasetId": "<dataset id of any Facebook Ad Library scraper run>",
    "datasetItems": [ { /* pasted scraper rows, any shape */ } ],
    "watchlistId": "acme-competitors",
                                  // optional: remember answered ads under this name (your own account)
    "newAdsOnly": false,          // ON: skip ads already on the list (or already delivered to your account) before download
    "maxAds": 1000,               // optional cap on what the whole run may bill; up to 10,000 — ask for more and the run continues at 10,000
    "includeImageText": true,     // default: image ads deliver their on-image text
    "includeOnScreenText": false  // opt-in: also read the text on screen in SILENT video 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, pageName, adArchiveId,
         searchQuery, hook3s, hookStartSeconds, ctaText, ctaType, transcript,
         imageText{headline,body,cta,rawText,truncated}, onScreenText{...,truncated},
         imageUrl, language, durationSeconds, segments[]
Pricing  pay per event, platform usage INCLUDED, no start fee:
  $0.030 -> $0.020 per delivered result (falls with your Apify plan tier) — one
  result = one video transcript OR one image-ad text extraction, same price
  ad-row  $0.004 -> $0.002 per delivered ad that comes with its Ad Library row (adLibrary)
  long-video-surcharge  $0.005 per started minute past the first 3 minutes (video only)
  Charged only on delivery — expired links, text-free images and failures cost $0. A silent
  or music-only ad is uncharged too, unless you turn includeOnScreenText on and it has readable
  on-screen text, in which case it delivers one result at the same price. Ads skipped by
  a watchlist (newAdsOnly) are never charged.
  Cap any run with maxTotalChargeUsd (minimum $0.05); ads the cap could not cover still
  ship as uncharged skipped_budget rows, so you always get one row per ad. An ad already
  being transcribed when the cap is reached is delivered in full, uncharged.
MCP:     ask Apify's MCP server for "facebook ad transcripts", or pin this actor
         in any MCP client with the one link at the top of this page.
```

### Why this Facebook ads transcript scraper?

- **Start with nothing.** You do not need ad IDs, links, or another scraper's dataset to get your first result. A keyword and a country is the whole input, and the ads come back already transcribed.
- **Charged only on delivery.** An ad's content either lands in your dataset or the row carries no result fee. Expired URL → uncharged row that says so. Music-only ad → uncharged row that says so. Text-free image → uncharged row that says so. No "it ran and charged me but returned nothing."
- **Pay once per ad, ever.** An ad already delivered to your account comes back from that run uncharged on every re-run, in any mode you name your own ads in (the default sample always runs live and is the one exception). With a watchlist and New ads only, re-running the same competitors delivers only what is new. Monitoring two hundred advertisers every morning costs the same as the handful of ads they launched overnight.
- **Every ad's content, not just the videos.** Competitor pages run image and video creatives side by side; this actor returns the video transcripts AND what the image ads say — one run, one price per delivered result. Metadata-only scrapers hand you image URLs; this hands you the words on them.
- **Silent ads need not be a dead end (optional).** Plenty of Ad Library video creatives carry no speech at all — kinetic typography, a static card, music over b-roll — and their whole message is on the screen. Turn on **Read on-screen text on silent ads** and those come back as `onScreenText` instead of an empty row. It is **off by default**, so nothing changes unless you ask for it, and a silent ad with no readable text on screen is still uncharged.
- **Any video length.** VSL-length creatives (7+ minutes, an hour if you like) transcribe fine — long videos add a small per-minute surcharge instead of failing or being silently cut at 2 minutes.
- **Bulk, not one-at-a-time.** Feed it your whole Ad Library scrape — hundreds of ads in one run, processed concurrently.
- **Hook + CTA as data, and the hook is never blank.** `hook3s` is the ad's first 3 seconds of speech — the line media buyers actually study — as its own field, with full segment timestamps beside it. Facebook creatives that open on music or a logo sting 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 input deep-scans your dataset rows for the video URL and ad metadata — no field mapping, no glue code.

### How to transcribe Facebook ads (no code)

**No ad links yet? Start here.**

1. Put a keyword or an advertiser's page name into **Search keywords** — e.g. `fitness app`, or `Nike`.
2. Pick the **Country** whose Ad Library you want searched (`US` by default), and how many ads each keyword may return.
3. Click **Start**. The ads running right now come back already transcribed, each row tagged with the keyword that found it.

**Just have a few ads in mind?**

1. In the Ad Library, open an ad and copy its link (it looks like `facebook.com/ads/library/?id=123456789`).
2. Paste one or many into **Ad Library links**. The actor finds each ad's creative for you.
3. Click **Start**. Each ad comes back as one JSON row — a video's transcript + hook, or an image's on-image text — plus CTA + metadata.

**Doing it in bulk?**

1. Run any Facebook Ad Library scraper (e.g. the ones you already use) — video and image ads both count.
2. Open this actor, paste your run's **dataset ID** (or use Apify's *Connect Actor* integration to chain it automatically).
3. Click **Start**, then export as JSON/CSV, or read it via API.

**Watching competitors over time?** Add a **Watchlist name**, turn on **New ads only**, save it as a Task and schedule it — the recipe above.

Prefer URLs? Paste `video_hd_url` / `video_sd_url` values into **videoUrls** directly.

Holding the video or audio file somewhere else — your own storage, a CDN, a signed link? This actor reads Meta ad creatives from Facebook's CDN, so pass that file to our media transcriber ([apify.com/steadyfetch/media-transcriber](https://apify.com/steadyfetch/media-transcriber)) instead and paste the link into its **Media URLs** (`urls`) field.

### How it compares

Prices and features as published on each product's own page or pricing tab on 2026-09-01; check them before you decide.

| | What you get | Transcripts | Any video length | Bulk / chaining | Watchlist, new ads only | Price as published |
|---|---|---|---|---|---|---|
| **This actor** | Transcript + hook + CTA + metadata per video ad; on-image text per image ad; on-screen text on silent ads (opt-in) | Yes | Yes — 3 min included, then $0.005 / started min | Search, links, video URLs, dataset chaining | Yes | $0.030 → $0.020 per delivered ad by plan tier, plus a small Ad fee on ads with their Ad Library row, usage included, no start fee |
| [Facebook Ad Library Scraper (curious_coder)](https://apify.com/curious_coder/facebook-ads-library-scraper) | Ad metadata and media URLs, in bulk | No — chain its dataset into this actor | — | Yes | No | $0.00075 per ad + $0.00005 per run |
| [Facebook Ads Transcript Scraper (automation-lab)](https://apify.com/automation-lab/facebook-ads-transcript-scraper) | Per its listing, items extracted from ads | Per its listing | Not stated | Not stated | No | $0.005 per run + $0.00045 per item (free plan) |
| Foreplay | An ad-spy app: swipe files, boards, brand tracking | In-app | In-app | App, not an API | Brand tracking in-app | $59, $175 or $459 per month |

The honest summary: if you want a **spy tool with a UI**, Foreplay is one. If you want the **data as rows** — to build your own board, feed an LLM, or track hundreds of advertisers with a script — this actor is the only one of the four that transcribes any length of ad, chains from any scraper, and only ever charges for what is new.

### Why do Facebook ad video URLs expire?

Facebook's video CDN (`video.*.fbcdn.net`) signs every URL with an expiry — typically a few hours to a few days (the `oe=` parameter is a hex timestamp). **Transcribe soon after scraping**: a dataset from last week will have dead links. Expired rows come back as uncharged `unavailable_expired` so you can re-scrape and re-run just those.

**Ad Library links don't expire like that** — paste `facebook.com/ads/library/?id=…` into **Ad Library links** and the actor looks the ad up and fetches a fresh video URL itself. Two honest caveats: an ad that's been taken down (or never got impressions) is simply no longer there, and comes back as an uncharged `unavailable_removed` row; and if Facebook temporarily declines a lookup, you get an uncharged `resolve_blocked` row telling you to re-run rather than a wrong answer. Both are **never charged**. This depends on Ad Library internals that Facebook can change without notice — if that happens, links fail as uncharged rows and the video-URL and dataset chaining routes keep working.

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

**One result = one ad's full content payload** — a video ad's transcript (+ hook + CTA + metadata + segments), an image ad's on-image text (+ CTA + metadata), or (opt-in) a silent video ad's on-screen text — at the same per-result price. The first 3 minutes of each video are included; longer videos add a small per-started-minute surcharge (never on images). No start fee, no subscription, no third-party API key to buy.

| Job | Approx. cost on the Apify free plan, Ad fee included |
|---|---|
| 50 competitor video ads | ≈ $1.70 |
| 500 ads (a serious creative teardown) | ≈ $17 |
| A daily watchlist over 20 competitors, ~15 new ads a day | ≈ $0.51 a day — the ads you already have are never billed again |
| Apify free plan ($5 credit) | ≈ **145 ad transcripts** |

Platform usage (compute + transfer) is **included in the event price** (since July 5, 2026) — no separate usage line on your Apify bill, so the per-transcript price you see is the whole price. Failed or empty fetches still charge nothing.

**For a faster run, raise the memory.** A run starts at 2 GB by default. Set **Memory** to 4 GB under Run options before you press Start and the run gets twice the processing power, so each video's sound is pulled out sooner, which is what long videos need (the transcription itself runs at the same pace). 4 GB is the most this actor uses. The price per ad is the same at any memory, because platform usage is already included.

**Your Apify plan decides which column you pay, and the run itself tells you which one it was.** A result is $0.030 on the Apify free plan, $0.0255 on Bronze, $0.025 on Silver and $0.020 from Gold — $30.00, $25.50, $25.00 and $20.00 per 1,000. A run billed at the free-plan column says so on its own run page and names what the same run costs on a paid one — on a crowded summary that sentence is the first thing left out, since this page carries the full price list; the plan is Apify's, not this actor's, and nothing about what is collected or delivered changes with it.

**Each delivered ad that comes with its Ad Library row also carries an Ad fee.** That is every ad found by keyword, Ad Library link or page link (the `adLibrary` field): $4.00 per 1,000 ads on the Apify free plan, $3.20 on Bronze, $2.50 on Silver and $2.00 from Gold. An ad from a video URL or a chained dataset has no Ad Library row and carries no Ad fee, and an ad that is not delivered carries none either. These prices have applied since 13 October 2026 at 22:12 UTC.

### Use it as an API endpoint (Standby)

You can also call this actor like an API: send the same input a run takes and get the ads back, transcribed, in the response itself — no run to start and no dataset to read. The address is `https://steadyfetch--facebook-ads-transcript-scraper.apify.actor`, also shown on the actor's **Endpoints** tab, where you can try it from the browser.

```bash
curl -H "Authorization: Bearer <YOUR_APIFY_TOKEN>" \
  "https://steadyfetch--facebook-ads-transcript-scraper.apify.actor/?searchQueries=fitness%20app&country=US&maxAds=5"
```

Use `GET` with the settings in the address (repeat `searchQueries`, `adLibraryUrls` or `videoUrls` for more than one), or `POST` the run input as JSON — `{"searchQueries": ["fitness app"], "country": "US", "maxAds": 5}` — including `datasetItems` with Ad Library rows. One request processes up to 20 ads (3 on the Apify free plan); for a larger batch, run the actor as usual.

The answer is JSON: `ads` (one row per ad, with the same fields as a row in a run's dataset), `delivered`, `charged` (how many of each event), `summary` (the same summary a run writes) and `notes` for anything that was not delivered, in plain words. You are charged per ad delivered at the same per-event prices as a run; an ad that returns no transcript or text is not charged, and if the charge for a request cannot be completed, its ads are not sent and nothing is charged.

Good to know: the endpoint cannot open your own Apify account, so a `datasetId`, a watchlist and `newAdsOnly` work in a run, not here, and the endpoint does not skip ads your account already has — every request processes the ads it is sent.

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

- **API**: standard Apify run API — `POST .../acts/steadyfetch~facebook-ads-transcript-scraper/runs` with `{ "searchQueries": ["..."] }` or `{ "datasetId": "..." }`. Python/Node clients work as with any actor. Send the settings themselves as the run input; a run that receives them wrapped as `{"input": {...}}` still reads them and says so in one uncharged note row.
- **MCP**: one link pins this actor in Claude, Cursor, or any MCP client — the pin link is at the top of this page — or ask Apify's MCP server for "facebook ad transcripts".
- **n8n**: two free ready-made templates, no community nodes needed — **Facebook Ad Transcripts** (keyword search → transcribe → pipe to Sheets, 3-minute setup) and **Competitor ad teardown** (one competitor page → chained by dataset ID → one teardown row per ad: hook, CTA, transcript length), both listed on our profile website (the link on our Apify profile page). Add `watchlistId` + `newAdsOnly` to either template's input to turn it into a daily new-ads feed.
- **Make / Zapier**: call it as a regular Apify actor step right after your scraper node — same two HTTP calls as the n8n templates.
- **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.

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

This actor reads the **creative URL value** in each row (any `https://video…fbcdn.net/…` video link, or `https://scontent…fbcdn.net/…` image link), not a fixed field name, so it works with any Ad Library scraper that exports direct media links. Verified shapes on this store:

| Scraper | Where the video URL lives in its output |
|---|---|
| [curious_coder/facebook-ads-library-scraper](https://apify.com/curious_coder/facebook-ads-library-scraper) | `snapshot.videos[].video_hd_url` / `video_sd_url` |
| [apify/facebook-ads-scraper](https://apify.com/apify/facebook-ads-scraper) | `snapshot.videos[].videoHdUrl` / `videoSdUrl` |
| [igolaizola/facebook-ad-library-scraper](https://apify.com/igolaizola/facebook-ad-library-scraper) | `snapshot.videos[].video_hd_url` / `video_sd_url` (also `snapshot.cards[]`) |
| [automation-lab/facebook-ads-library](https://apify.com/automation-lab/facebook-ads-library) | `videoUrls[]` |
| [brilliant_gum/facebook-ads-library-scraper](https://apify.com/brilliant_gum/facebook-ads-library-scraper) | `creatives[].videoUrl` |

**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 to pass it automatically → Start. Using a scraper that isn't listed? If its rows contain the fbcdn video or image link, it works the same way — and if one doesn't, the row comes back as an uncharged `no_creative_found`.

### FAQ

**Is it legal to transcribe Facebook ads?** The Ad Library is public by design (ad transparency), and US courts have held that logged-out scraping of public Meta data does not breach Meta's terms (Meta v. Bright Data, 2024). The ad-spy industry (Foreplay, Atria, AdSpy) operates on this data openly. As always, how you use the data is on you.

**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 (`fb-ads-watch-<name>`), keyed by the ad's Ad Library ID (or the creative's file path 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.

**I ran the same ads twice — was I charged twice?** Not since 1.0.76: an ad already delivered to your account comes back from the run that delivered it, `repeat: true` and `charged: false`, however it arrived this time (a video URL, a link, a search result, a chained row). The default sample (Start with nothing set) is the exception: it always runs live, so pressing Start again charges for its three ads again. The memory lives in `fb-ads-watch-account` in your account; delete it to forget. It is a key-value store, so a run started with a scoped API token needs key-value store Read, Write and Create permission (or Actor runs set to Full access) to use it — without that the run says so and cannot skip what you already have.

**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 downloaded; the status line and `OUTPUT.skippedSeen` count them, and no row and no charge is produced for them.

**What happens on a failed download?** It is retried automatically; if the file still cannot be fetched you get an uncharged `failed_download` row plus an entry in the run's `ERRORS` record. You pay $0 for anything that didn't deliver — and with a watchlist, a failed ad is not marked as seen, so the next run tries it again.

**Music-only ads?** Speech-recognition models hallucinate filler ("Thank you.") on music. We detect that and return an honest, uncharged `no_audio` row instead of selling you a fake transcript — and the row points you at the fix. If that ad's message is written on the screen, turn on **Read on-screen text on silent ads** (`includeOnScreenText: true`) and it comes back as a charged `onscreen_text_extracted` row carrying `onScreenText` instead; with no readable text on screen it stays uncharged as `no_onscreen_text`. The option is off by default. The advertiser name on such a row always comes from the ad's own metadata, never from reading the picture.

**Image ads?** They deliver their on-image text — headline, body, CTA and the full visible text as `imageText` — charged like any other delivered result (same price, no surcharge). An image with no readable text (a plain product photo) is an uncharged `ocr_no_text_found` row, honestly labelled. Want the old pass-through instead? Set `includeImageText: false` and image ads ship uncharged with metadata only, marked `image_skipped`.

**Languages?** Multilingual speech recognition — Spanish, German, Arabic, and ~90 more transcribe out of the box; the detected `language` ships on every row.

**Can I paste Ad Library links?** Yes — that's the **Ad Library links** field. Give it `facebook.com/ads/library/?id=…` (or a bare ad ID) and the actor resolves the ad's video itself. A link from an advertiser's Ad Library **page** (the address bar on their page — no `?id=` in it) points at all their ads rather than one, and that works too: paste it and the run searches that advertiser's ads, under the country, format and status you set, charging each ad found exactly as a keyword search would. One uncharged note row says it was read that way. Links it can't resolve are uncharged rows that say why: `unavailable_removed` (the ad is gone) or `resolve_blocked` (a temporary block — re-run shortly).

**Why not scrape the whole Ad Library here too?** Single-purpose by design: paste the ads you care about, or let your scraper of choice find them in bulk; this actor turns them into transcript data, reliably. One job, done honestly.

**Is this an official Meta product?** No. This is an independent actor from steadyfetch — unofficial, and not affiliated with, endorsed by, or sponsored by Meta Platforms, Inc. "Meta", "Facebook" and "Instagram" are trademarks of their respective owners, 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 | **this actor** | Facebook ad transcripts · Competitor ad teardown |
| Google Ads Transparency Center | [Google Ads video transcripts](https://apify.com/steadyfetch/google-ads-video-transcript-scraper) | Google Ads video transcripts |
| TikTok Creative Center Top Ads | [TikTok Top Ads transcripts](https://apify.com/steadyfetch/tiktok-ads-transcript-scraper) | TikTok Top Ads transcripts |
| LinkedIn Ad Library | [LinkedIn ad transcripts](https://apify.com/steadyfetch/linkedin-ads-transcript-scraper) | LinkedIn Ad Library transcripts |
| Instagram Reels (organic posts) | [Instagram Transcript Scraper — Instagram Reels Transcript API](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 Ad Copy Scraper — Ads Transparency Center, CTAs & 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.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

# Changelog

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

# Actor input Schema

## `searchQueries` (type: `array`):

Keywords or advertiser page names to search, e.g. \["fitness app"] or \["Nike"] — start here if you have no ad IDs; each keyword returns the ads currently running in the country below, already transcribed. Keywords that match nothing ship as clearly-labelled rows and are never charged. What matches: an advertiser's page name, spelled as it appears on their Facebook page — `Nike`, `Acme Fitness`, `Rival Gym App` — or words that appear in the ads themselves, like `fitness app`. A website address (`nike.com`) is not a page name and finds nothing; a single ad's ID or link belongs in "Ad Library links" (`adLibraryUrls`), not here. The country below is part of the match, so an advertiser running ads in one market only needs that country set. Leave everything empty and click Start and the run is a 3-ad sample (currently running US video ads matching "fitness app", charged like any run) — the fastest way to see real output. Change only the settings below and leave this empty and that sample runs under your settings, with one uncharged note row saying which ones it used.

## `country` (type: `string`):

Which country's Ad Library to search. The two-letter code (US, GB, DE, IN, SA …), the three-letter code (USA, GBR, IND), the country's English name (India, "United Kingdom") and its name in its own language (Deutschland, España, 日本, مصر) all work, and ALL (or "worldwide") searches every country — where the run has to read a spelling it tells you on an uncharged note row which market it searched. It scopes every search this run makes: search keywords AND any advertiser Ad Library PAGE link you paste. A page link's own country is not read — set it here. An Ad Library SEARCH link uses its own country, and this one only where the link names none. Ad links, dataset rows and video URLs name one ad each, so this does not apply to them.

## `mediaType` (type: `string`):

"video" (default) returns video ads to transcribe. "all" also returns image ads, whose on-image text is read instead. Only used with search keywords.

## `activeStatus` (type: `string`):

"active" (default) returns ads running right now — what you usually want for competitor research. "all" also includes ads that have stopped running. Only used with search keywords.

## `searchMaxAds` (type: `integer`):

How many ads each search keyword may return (default 25). Keeps a broad keyword from turning into a large bill. Up to 500 per keyword; ask for a bigger number and the run continues at 500, with one uncharged row saying so.

## `adLibraryUrls` (type: `array`):

Paste links to individual ads, e.g. https://www.facebook.com/ads/library/?id=123456789 (the ad's "Copy ad link", or your browser's address bar when an ad is open). Links it can't resolve — removed ads, or a temporary block — ship as clearly-labelled rows and are never charged. The actor finds the ad's video for you. A bare ad ID works too. An advertiser's Ad Library PAGE link (the address bar on their page — it has no `?id=` in it) works too: it is read as a search of that advertiser's ads, under the country, format and status you set, and each ad found is charged exactly as a keyword search would be. An Ad Library SEARCH link (the address bar after you search — it has q= in it) is read as that search: its words, country (ALL included), exact-phrase setting, status and format, with one uncharged note row saying how it was read, and charged the same way.

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

Facebook video CDN links (video.\*.fbcdn.net) — the video_hd_url / video_sd_url field from any Ad Library scraper result; they expire within hours to days. An ordinary Facebook video, reel or fb.watch link is not an ad-library link: paste it into steadyfetch/media-transcriber, field urls. Any other host's file goes there too. (Ad Library links pasted here are handled for you: an ad link is resolved, an advertiser's page link is read as a search of that page.) A file on any other host (your own storage, a CDN, a signed link) is not read here — pass it to our media transcriber (apify.com/steadyfetch/media-transcriber) and paste the link into its "Media URLs" (urls) field.

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

The default dataset ID of a finished ad-library scraper run: the first 10,000 rows are read and up to maxAds ads (1,000 by default, 10,000 at most) are processed per run. Ads this account already has are handed back uncharged and do not count against maxAds, so a re-run of the same dataset carries on with what is new. 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 the ad's video or image URL + metadata (CTA, page name, ad archive ID) — works with any scraper's output shape. Leave it empty to search by keyword or use the other inputs instead.

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

Reads the on-image text of image ads and delivers it as `imageText`, charged like a video transcript (same event, same price; never a long-video surcharge) — ON by default. Images with no readable text are never charged. OFF: image ads pass through uncharged as `image_skipped` rows with their metadata, as before.

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

Reads the text on screen in a silent or music-only video ad and delivers it as `onScreenText` — many silent ads put their whole message on the screen. OFF (default): such an ad ships as an uncharged `no_audio` row. A delivered on-screen extraction is charged like a video transcript (same event, same price; never a long-video surcharge). A silent ad with no readable on-screen text stays uncharged.

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

Names a watchlist to follow an advertiser or keyword across scheduled runs: a short name, e.g. `acme-competitors` (letters, numbers and hyphens). The list lives in a named key-value store in your account called `fb-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`):

Skips ads already on your watchlist — or, with no list name, already delivered to your account — before download: not fetched, not charged, and the run's status line and OUTPUT say how many. 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`. 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 failed download, a link it could not resolve, an ad your cost cap left out — does not, so the next run tries it again.

## `maxAds` (type: `integer`):

Safety cap on how many ads to transcribe in one run (default 1,000). Up to 10,000 per run; ask for a bigger number and the run continues at 10,000, with one uncharged row saying so. 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
{
  "country": "US",
  "mediaType": "video",
  "activeStatus": "active",
  "searchMaxAds": 25,
  "includeImageText": true,
  "includeOnScreenText": false,
  "newAdsOnly": false,
  "maxAds": 1000
}
```

# Actor output Schema

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

One row per ad, with the search keyword that found it. Video ads: transcript, opening hook, CTA, advertiser, duration. Image ads: the on-image text (headline, body, CTA, raw text). Every row carries a status (transcribed / image_text_extracted / no_audio / unavailable_expired / input_note / …); only delivered rows are charged, and `charged` on each row says whether it was — an ad already being transcribed when your maximum cost is reached still delivers in full, uncharged. Two ad IDs running the SAME video are one creative: both rows are delivered with the transcript in full and only the first is charged, and the second row says which ad it repeats. An `input_note` row is the run saying what it read from your input — an advertiser's Ad Library page link read as a search of that page, or another actor's field name accepted here — so the input worked, and the note is never charged. 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 download and never charged. Ads your account already had come back from the run that delivered them, `repeat: true` with `firstSeenRunId`, not charged; the default sample (Start with nothing set) always runs live instead and is charged like any run. Every ad found by keyword or resolved from an Ad Library link also carries `adLibrary` — the rest of that ad's Ad Library row: when it started and stopped, whether it is still running, the platforms it runs on, its landing page, headline and caption, the advertiser's page ID, profile, categories and follower count, the collation it belongs to, and the EU spend and impressions bands where Meta publishes them. An ad that never came from the Ad Library — a pasted video URL, a chained scraper row — has `adLibrary: null`, because we did not look it up rather than because it said nothing.

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

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

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

Present only when downloads/ASR failed after all retries: video URL 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/facebook-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/facebook-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/facebook-ads-transcript-scraper --silent --output-dataset

```

## MCP server setup

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