Facebook Ads Transcript Scraper — Ad Library Video & Image Ads
Pricing
from $10.00 / 1,000 ad creative transcripts
Facebook Ads Transcript Scraper — Ad Library Video & Image Ads
Facebook & Instagram Ad Library ads transcribed in bulk: video transcripts with the first-3s hook, plus image-ad text. Search by keyword or advertiser — no ad IDs needed — or paste ad links or chain any scraper. No start fee; expired links, text-free images and music-only ads are never charged.
Pricing
from $10.00 / 1,000 ad creative transcripts
Rating
0.0
(0)
Developer
Steadyfetch Team
Maintained by CommunityActor stats
1
Bookmarked
57
Total users
22
Monthly active users
5.8 hours
Issues response
2 hours ago
Last modified
Categories
Share
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 $10.00 per 1,000 ad creatives on the Business plan ($20.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.
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.06 on the free plan, $0.03 on Business). 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. 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. 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).

| 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 |
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.

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:
{"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 → — 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.
Image ads deliver as image_text_extracted rows: the same metadata plus imageText — { headline, body, cta, advertiser, displayUrl, rawText }, read straight off the creative — and the ad's imageUrl. 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 reached its time limit before this ad's turn — the same uncharged row, naming the run timeout instead of the cap; 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 / ocr_service_unavailable (the image's text could not be read reliably 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 — one attempt, then an uncharged row), no_creative_found, unavailable_removed, resolve_blocked, search_no_results (a keyword no ad matched right now — the row suggests what to widen), 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 — an advertiser's Ad Library page link read as a search of that page, or a field name from another one of our actors accepted here; the input worked 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). 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) 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.
- Put your keywords or advertiser names in Search keywords (or paste Ad Library links, or chain a scraper run — any input mode works).
- Give the run a Watchlist name, e.g.
acme-competitors, and turn on New ads only. - 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.
{"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/firstSeenAton 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. 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. - 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).
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 (ortake ad links / a chained dataset) -> every ad's content. Video ads:full transcript + opening hook, any length. Image ads: theon-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_TOKENInput (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"adLibraryUrls": ["https://www.facebook.com/ads/library/?id=1265622684930223"],// Ad Library links or bare ad IDs; the actor finds the video"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"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 thedefault" — so a template that renders unset fields as null (n8n, agentframeworks, 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}, onScreenText{headline,body,cta,rawText},imageUrl, language, durationSeconds, segments[]Pricing pay per event, platform usage INCLUDED, no start fee:$0.020 -> $0.010 per delivered result (falls with your Apify plan tier) — oneresult = one video transcript OR one image-ad text extraction, same pricelong-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 silentor music-only ad is uncharged too, unless you turn includeOnScreenText on and it has readableon-screen text, in which case it delivers one result at the same price. Ads skipped bya watchlist (newAdsOnly) are never charged.Cap any run with maxTotalChargeUsd (minimum $0.05); ads the cap could not cover stillship as uncharged skipped_budget rows, so you always get one row per ad. An ad alreadybeing transcribed when the cap is reached is delivered in full, uncharged.MCP: pin this actor in any MCP client with https://mcp.apify.com?tools=steadyfetch/facebook-ads-transcript-scraper(or ask Apify's MCP server for "facebook ad transcripts").
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
onScreenTextinstead 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.
hook3sis 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, andhookStartSecondstells you which second that is (0when the ad opens speaking). An ad with no speech at all leaves bothnull— 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 byhookStartSecondsto 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.
- Put a keyword or an advertiser's page name into Search keywords — e.g.
fitness app, orNike. - Pick the Country whose Ad Library you want searched (
USby default), and how many ads each keyword may return. - 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?
- In the Ad Library, open an ad and copy its link (it looks like
facebook.com/ads/library/?id=123456789). - Paste one or many into Ad Library links. The actor finds each ad's creative for you.
- 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?
- Run any Facebook Ad Library scraper (e.g. the ones you already use) — video and image ads both count.
- Open this actor, paste your run's dataset ID (or use Apify's Connect Actor integration to chain it automatically).
- 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) 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.020 → $0.010 per delivered ad by plan tier, usage included, no start fee |
| Facebook Ad Library Scraper (curious_coder) | 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) | 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) |
| ScrapeCreators (API) | A general social-scraping API with a Facebook ad-transcript endpoint | Yes | Not stated | Your own code; no Apify chaining | No | Credit packs from $47 per 25,000 credits (about $1.88 per 1,000 requests) |
| 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 five 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 |
|---|---|
| 50 competitor video ads | ≈ $1.00 |
| 500 ads (a serious creative teardown) | ≈ $10 |
| A daily watchlist over 20 competitors, ~15 new ads a day | ≈ $0.30 a day — the ads you already have are never billed again |
| Apify free plan ($5 credit) | ≈ 250 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.
Use it via API, MCP, and integrations
- API: standard Apify run API —
POST .../acts/steadyfetch~facebook-ads-transcript-scraper/runswith{ "searchQueries": ["..."] }or{ "datasetId": "..." }. 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/facebook-ads-transcript-scraper— 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+newAdsOnlyto 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 | snapshot.videos[].video_hd_url / video_sd_url |
| apify/facebook-ads-scraper | snapshot.videos[].videoHdUrl / videoSdUrl |
| igolaizola/facebook-ad-library-scraper | snapshot.videos[].video_hd_url / video_sd_url (also snapshot.cards[]) |
| automation-lab/facebook-ads-library | videoUrls[] |
| 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.
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 Transcript Scraper — Ads Transparency Center | Google Ads video transcripts |
| TikTok Creative Center Top Ads | TikTok Ads Transcript Scraper — Creative Center Top Ads & Hooks | TikTok Top Ads transcripts |
| LinkedIn Ad Library | LinkedIn Ads Scraper — Ad Library Transcripts & Image Ad Text | LinkedIn Ad Library transcripts |
| Instagram Reels (organic posts) | Instagram Reel Transcript Scraper — Reels to Text with 3s Hooks | 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 |
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 · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR |
| Trends & keywords | Google Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends |
| YouTube transcripts | YouTube videos · YouTube channels |
| Reel transcripts · Profile posts | |
| Jobs | Indeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs |
| Amazon | Products · Search · Bestsellers · Sellers |
| Any media file | Speech to Text · any link or file |
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.