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

Facebook Ads Transcript Scraper — Ad Library Video & Image Ads

Pricing

from $10.00 / 1,000 ad creative transcripts

Go to Apify Store
Facebook Ads Transcript Scraper — Ad Library Video & Image Ads

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

Steadyfetch Team

Maintained by Community

Actor stats

1

Bookmarked

57

Total users

22

Monthly active users

5.8 hours

Issues response

2 hours ago

Last modified

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

The input form: search keywords, country, Ad Library links, and the watchlist fields

You give itYou get back
A keyword or advertiser name + a country — nothing else neededtranscript — 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 modeads 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):

statuschargedhook3shookStartSecondstranscriptlanguagedurationSeconds
unavailable_expiredfalsenullnullnullnullnull
transcribedtrueHice este desafío de Pelatis en la pared de 28 días para mi boda y ahora lo haré0Hice 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…Spanish40.00
transcribedtrueYour sister's on the phone.0Your 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…English36.71
transcribedtrueLook, women are always right. My girlfriend told me to get the Native Facial Hyd0Look, 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…English32.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

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.

  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.
{
"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. 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 (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
"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 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}, 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) — one
result = one video transcript OR one image-ad text extraction, same price
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: 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 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) 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 getTranscriptsAny video lengthBulk / chainingWatchlist, new ads onlyPrice as published
This actorTranscript + hook + CTA + metadata per video ad; on-image text per image ad; on-screen text on silent ads (opt-in)YesYes — 3 min included, then $0.005 / started minSearch, links, video URLs, dataset chainingYes$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 bulkNo — chain its dataset into this actorYesNo$0.00075 per ad + $0.00005 per run
Facebook Ads Transcript Scraper (automation-lab)Per its listing, items extracted from adsPer its listingNot statedNot statedNo$0.005 per run + $0.00045 per item (free plan)
ScrapeCreators (API)A general social-scraping API with a Facebook ad-transcript endpointYesNot statedYour own code; no Apify chainingNoCredit packs from $47 per 25,000 credits (about $1.88 per 1,000 requests)
ForeplayAn ad-spy app: swipe files, boards, brand trackingIn-appIn-appApp, not an APIBrand 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.

JobApprox. 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/runs with { "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 + 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:

ScraperWhere the video URL lives in its output
curious_coder/facebook-ads-library-scrapersnapshot.videos[].video_hd_url / video_sd_url
apify/facebook-ads-scrapersnapshot.videos[].videoHdUrl / videoSdUrl
igolaizola/facebook-ad-library-scrapersnapshot.videos[].video_hd_url / video_sd_url (also snapshot.cards[])
automation-lab/facebook-ads-libraryvideoUrls[]
brilliant_gum/facebook-ads-library-scrapercreatives[].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 surfaceActorFree n8n template
Facebook & Instagram Ad Librarythis actorFacebook ad transcripts · Competitor ad teardown
Google Ads Transparency CenterGoogle Ads Video Transcript Scraper — Ads Transparency CenterGoogle Ads video transcripts
TikTok Creative Center Top AdsTikTok Ads Transcript Scraper — Creative Center Top Ads & HooksTikTok Top Ads transcripts
LinkedIn Ad LibraryLinkedIn Ads Scraper — Ad Library Transcripts & Image Ad TextLinkedIn Ad Library transcripts
Instagram Reels (organic posts)Instagram Reel Transcript Scraper — Reels to Text with 3s HooksInstagram Reel transcripts

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

Ad surfaceActor
Google Ads Transparency CenterGoogle 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.

FamilyActors
Ad creative intelligenceFacebook · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR
Trends & keywordsGoogle Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends
YouTube transcriptsYouTube videos · YouTube channels
InstagramReel transcripts · Profile posts
JobsIndeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs
AmazonProducts · Search · Bestsellers · Sellers
Any media fileSpeech 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.