# Changelog of Facebook Ad Library Scraper — Facebook Ads by Keyword & Country (`steadyfetch/facebook-ad-library-scraper`) Actor

- **URL**: https://apify.com/steadyfetch/facebook-ad-library-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/steadyfetch/facebook-ad-library-scraper.md

## Changelog

### 1.0.26 — 2026-10-17

- **A run no longer stops "at your max charge" with part of that limit unused.** With "Add the ad's transcript" on, the run decided how many ads to fetch before making any transcript, and counted every ad as one that would get one. An ad whose video had no speech or could not be downloaded, and every image ad, still came back with its row but with no transcript charged — yet the run never went back for more ads, so it could stop "at your max charge" while part of your limit was never spent, and image ads were held back as if each needed a transcript. The run now fetches more ads, in the same order, for as long as your limit still covers them, and stops only when it truly does not, when the search has no more ads, at "Max ads to return" or when the run's time is up; if the run's time ends first, the ads your limit still covered are named as stopped by the run timeout, not by the max charge. A run in which every ad gets its transcript works exactly as before. Nothing changes about which rows are charged, the input fields or the output columns.

### 1.0.25 — 2026-10-16

- **Short run timeouts now get their transcripts.** With "Add the ad's transcript" on, a run with a 60-second timeout delivered every ad row but skipped every transcript and asked for "at least 2 minutes", although each transcript takes only a few seconds: the run kept a full minute back for its closing report whatever its timeout was. A run timeout under two minutes now keeps back a quarter of itself for that report, so each transcript that fits is made and charged as usual. A transcript that truly cannot fit still comes back uncharged, and its note now names the exact run timeout it needs, in seconds; the ad row itself is delivered and charged as always. A transcript whose download ran out of the little time a short run had left now says the run's timeout was too short to finish it and names the timeout that would, instead of asking you to re-run. Ads a short run had no time left to reach name the run timeout that reaches them, on their own rows and on the run page ("3 ads left: set it to at least 107 s") instead of "raise it". Runs with a timeout of two minutes or more work exactly as before. Nothing changes about which rows are charged, the input fields or the output columns.

### 1.0.24 — 2026-10-15

- **The two date filters now read the dates you actually type.** "Only ads that started on or after" and "Only ads that started on or before" took only a date written YYYY-MM-DD, and any other way of writing a day stopped the search with a row asking you to try again. They now also take 2026/01/31, 31/01/2026, 31 Jan 2026 or October 1, 2026, a timestamp such as 2026-01-31T09:00:00Z (read as its day in UTC), and a span counted back from the run's start: "7 days ago", "last week", "last 30 days", "yesterday" or "today" — so a saved task with "7 days ago" always means the last seven days. When a date is not written YYYY-MM-DD, one row (never charged) shows the day it was read as. A day and month that could be read either way, such as 01/10/2026, is not guessed: the run tells you both readings and searches nothing, and a value that is not a date at all is turned away exactly as before, uncharged. A date written YYYY-MM-DD works exactly as before. Nothing changes about the price, which rows are charged, or the output columns.
- **A busy speech service no longer costs you transcripts.** With "Add the ad's transcript" on, when the speech service asks the run to slow down, the transcript it turned away is held and made as soon as the service answers, for as long as the run's own time allows, and charged once, exactly as if it had been made the first time. Only when the run's time runs out does the transcript come back with a note that the service was busy, with no transcript charged; the ad row itself is delivered and charged as always. A transcript the service turned away also no longer counts toward this run's limit, so a busy spell cannot stop later transcripts in the same run. Nothing changes about the price, which rows are charged, the input fields or the output columns.

### 1.0.23 — 2026-10-14

- **Ad Library search links now run as searches, and "ALL" now means every country.** Paste the address of an Ad Library search (for example `facebook.com/ads/library/?q=…&country=ALL&search_type=keyword_exact_phrase`) into "Ad Library links" — or into "Search keywords" — and the run searches exactly that: its words, every country or the one it names, exact phrase or any order, the ad status and format it sets; a setting the link does not carry comes from your own input, and one row (never charged) says how each link was read. A search link this actor cannot fully apply — one with no search words, or limited to stopped ads, image-only ads, one ad category, a date range, platforms or languages — is turned away on its own row that says why, instead of being searched without that limit. "Country" now also takes `ALL` (or "worldwide") to search every country. Links that name one ad or an advertiser's page work exactly as before, and each kind of Ad Library link that cannot be used now gets its own accurate explanation. Nothing changes about the price, which rows are charged, or the output columns.

### 1.0.22 — 2026-10-11

- **Clearer wording when a run stops at its limit.** With "Add the ad's transcript" on, an ad whose transcript this run could not make now carries the transcript status "limit_reached", and its note says "This run reached its limit", with how to fetch the remaining transcripts in a new run. Nothing changes about the price, which rows are charged, the input fields or the other output columns.
- **A transcript still being made no longer counts toward this run's limit.** With "Add the ad's transcript" on, a transcript still in progress was counted toward this run's limit as if it had produced nothing, so after a stretch of ads with no speech a later ad could be marked "limit_reached" on a run where every transcript made was charged. Only ads whose video produced no transcript now count toward that limit. Nothing changes about the price, which rows are charged, the input fields or the output columns.

### 1.0.21 — 2026-09-26

- **An ad this run's own spending limit stopped now says so plainly.** A transcript not attempted because this run reached its own budget for this run on media that yielded nothing chargeable, and that a re-run continues now says exactly that. The run also now notes its own start in its internal record, so a run cut off before it finishes (a hard abort, a crash or a timeout) is still accounted for on our side. Nothing changes about what is delivered, which rows are charged, the price, the input fields or the output columns.

### 1.0.20 — 2026-09-24

- **With "Include transcript" on, short run timeouts now transcribe the ads they have time for.** A run with a timeout under about 6 minutes used to leave every transcript as `skipped_deadline`, even when each video needed only seconds. A transcript is now started whenever the run's remaining time can realistically download and transcribe it: about 1 minute 30 seconds of run time left for a typical ad, more for a long video once its length is known. One that truly cannot fit is still skipped uncharged, and its row now says how much time was left, how much the video needs and the run timeout to set. Ad rows are delivered and charged exactly as before. No price, charged event, input field or output column changed.

### 1.0.19 — 2026-09-24

- **A run that Apify restarts after it has already delivered keeps its own record of that delivery.** If the restarted part of a run delivers nothing new, the run's internal record now keeps what the earlier part delivered and charged, with a note of the restart, instead of being rewritten as if nothing had been delivered; one line in the run log says so. Nothing changes about what is delivered, which rows are charged, the price, the input fields or the output columns.

### 1.0.18 — 2026-09-22

- **The run log no longer prints an internal usage summary at the end of a run, and a failed internal bookkeeping write no longer prints where it was being saved.** Nothing changes about what is delivered, which rows are charged, the price, the input fields or the output columns.

### 1.0.17 — 2026-09-23

- **A run close to your "Maximum cost per run" no longer marks a long-video minute as charged when your cap cannot pay for it.** With transcripts on, one ad can owe three things — the ad row, its transcript and, past the first three minutes of video, one charge per started minute — and they are billed in that order, after the row is written. The check for the minutes counted the transcript in front of them but not the ad row, so with a few cents of your cap left an ad could say in `chargeEvents` that it owed a minute the cap no longer covered, and the run could then be stopped by the platform for going over your maximum. With several ads in progress at once, the same gap could also let one ad's transcript be bought on money another ad's minutes had already taken. Every charge is now checked against what your cap still holds after every charge already decided — this ad's and those of the ads being worked on beside it. A minute that does not fit is not charged and the row says 0; a transcript that does not fit is not fetched, as before; the ad row is never the part that gives way. A run whose cap covers everything is billed exactly as before. No price, input field, output column or charged event changed.

### 1.0.16 — 2026-09-22

- **A failed transcript row no longer names or quotes our upstream services.** When the speech service turns a request away, the row now says so in plain words instead of passing on that service's own message and links. A verdict about your own file ("file is empty") still reads as before. Nothing about charging changed.

### 1.0.15 — 2026-09-22

- **A date window that your advertiser's newer ads sit in front of now returns the ads inside it, and a run that delivers nothing is never called a repeat run.** The Ad Library serves a keyword's ads a page at a time, and "started on or after" / "started on or before" is applied to the ads on each page — so a window whose ads sit three or four pages down is reached only by reading past the ones ahead of it. Those pages delivered nothing, and the run counted them as pages that had added nothing new: two of them ended the search, and the uncharged note said it had stopped on pages holding only ads it had already listed — on runs with a single keyword, where nothing could have been listed twice. A search now reads on toward your dates, up to as many pages as the number of ads you asked for needs; a page your own dates emptied no longer counts against the stop that exists for a feed repeating itself; and a search that reaches the pages this actor buys for one search without meeting your window says so in those words, on its own uncharged row, instead of reporting it as a verdict about the advertiser. The count of ads your dates dropped was already on the run page and is unchanged. No price, input field, output column or charged event changed, and ads a date window drops are still never fetched or charged.

- **A country named in its own language is now read, and the note about it no longer says your run searched nothing when it did.** "Country" took the two-letter code, the three-letter code or the country's English name — so a German buyer typing `Deutschland`, or a Spanish one typing `España`, lost the market they came for and got an uncharged row telling them to try again. Those spellings now work, in every language the country is named in, and the note that shows you which market was read says so. Separately, that refusal always ended "No search was run and nothing was charged" — a sentence written for a run built from search keywords. On a run of pasted Ad Library ad links, which country never applied to in the first place, it sat beside ads the run had looked up and charged for and said the opposite of what happened. The note now says what the run actually did: on a keyword or advertiser-page run it still says the search was stopped and nothing charged, on a links-only run it says the country scopes searches only and points you at each ad's own row for what it was charged, and where the country was understood and simply did not apply it says nothing at all. Nothing about the price, the charged events, the input form or any output column changed.

### 1.0.14 — 2026-09-22

- **A run billed at the Apify free plan column now says so on its own run page, and names what the same run costs on a paid plan.** This actor's prices fall with your Apify plan — an ad row is $0.004 on the free plan, $0.0032 on Bronze, $0.0025 on Silver and $0.002 from Gold, and a transcript $0.020, $0.016, $0.0125 and $0.010 — and the whole table has been on the store page since the first build. The run page, which is the surface you are actually looking at while a run spends your credit, said nothing about it. The status line now carries one clause naming the column that billed the run and what each paid rung costs, and the transcript price is quoted only on a run that asked for transcripts, because on any other run it is a price you were never charged. Every number in it is read from the same table the run charges from, so it cannot drift from your bill. On a crowded run the clause is the first thing dropped to make room — it is a standing price the store page carries in full, while everything else on that line is a fact about your run. A run on a paid plan reads exactly as it did before. No price, input field, output column or charged event changed.

### 1.0.13 — 2026-09-21

- **With transcripts on, every ad in a long run is now attempted — before this build a run stopped transcribing after about seventy, whatever it had already charged you for.** This actor holds a per-run limit on transcript work that comes back with no transcript, so that a wall of silent or unusable creatives cannot keep buying transcription for the whole run. That limit was supposed to grow with what a run had actually charged, and it never did: it sat at its starting value from the first ad to the last, which on a long run turned it into a hard ceiling at roughly seventy transcripts. Past that point every remaining ad delivered its row with `transcriptStatus` reading `vendor_budget` and no transcript — on runs that had charged for all seventy of the transcripts before it. The limit now rises with the transcripts a run actually delivers, so a run that is paying its way keeps transcribing to the end, while a run whose videos really do come back silent still stops exactly where it always did, uncharged. The row that reports the limit also says what is true here now: the ad row above it was delivered and charged as normal, nothing was charged for its transcript, and, since re-running the same search lists and charges its ad rows again, it points at the `adLibraryUrl` values to put into "Ad Library links" to collect just the transcripts a run left. No price, input field, output column or charged event changed.

### 1.0.12 — 2026-09-19

- **The whole "Maximum cost per run" you set is now spent on ads.** A capped run used to hold back about a tenth of your cap as a cushion against platform usage — but your cap pays for the ad rows and the opt-in transcripts and nothing else, so that tenth was cap you had asked to spend and did not get: a cap worth eleven ads delivered ten. A run now delivers every ad its cap can pay for, and still ends the way it always has — on its own line naming your cap and the exact count it cut, never on a run the platform cuts short, and never buying a search page it cannot sell you a row from. Nothing about the price, the charged events, the input form or any output column changed.

### 1.0.11 — 2026-09-19

- **A transcript step your own time limit cuts is now counted as cut by the time limit, not as a failure — and no step runs on a clock of its own any more.** Two local steps of the transcript add-on used to run on fixed clocks whatever the run had left: the quick inspection that reads a video's length before its audio is extracted, and the re-encoding step that shrinks an image that came back too large. Each now gets only the time the run actually has left, and when it is our clock that ends the step the ad's transcript is counted under your time limit, with the same uncharged row and the same advice to raise "Max run seconds" as every other time-limit stop. Nothing changes about what is delivered or charged, and a run with time to spare behaves exactly as before.

### 1.0.10 — 2026-09-18

- **When the run summary is too long to fit, what gives way is the support ask, never the sentence saying what you were not charged for.** The run page cuts its status line at 500 characters. Measured over the 120 shapes a run can end in, 40 of them were over that cut outright — the longest ran to 622 characters, so a buyer saw the sentence they needed replaced by "…" — and 62 lost a money sentence: the ads your date window dropped uncharged, the ads already delivered and never charged again, and the ads left out instead of charged for on the per-row rail could all be dropped, while "N beyond maxAds" — your own cut, which costs you nothing — could not, and an invitation to open an issue could never be dropped at all. Every sentence now declares whether it is about your money, each money sentence has a short form spent before it is ever dropped, and the ask goes first. "N keywords not searched" also lost a semicolon that was splitting one clause into two. No price, input field, output column or charged event changed.
- **An advertiser's page link is no longer searched under a setting this actor refused.** Pasting an Ad Library link to an advertiser's PAGE runs a search of that advertiser, under the country, ad format, ad status and date window you set. When one of those values could not be read the run refused it and stopped the keyword searches — correctly, because searching a different filter answers a different question and charges you for it — but the advertiser-page search went ahead anyway, and its note said in so many words that it had run "under the country, format and status you set". Both now stop the same way: one row names the forms the field accepts, and every advertiser and keyword stopped gets its own uncharged row saying so. A pasted link to ONE ad is untouched, because it depends on no filter.

### 1.0.9 — 2026-09-16

- **Type a country's name into "Country" and the run now searches that market, instead of refusing.** Until this build "Country" took a two-letter code and nothing else, so someone who typed "India" got an uncharged refusal row and no ads. The three-letter code (IND, GBR, USA), the country's full English name ("India", "United Kingdom", "Saudi Arabia") and the ordinary spellings people use (USA, UK, Great Britain, Holland, South Korea, UAE) are all read now, punctuation and accents included ("U.S.A.", "Türkiye"). Where the run has to read a spelling rather than a code it says so on one uncharged note row — "Read "India" as IN" — so you always see which market was searched and which code to set next time. A value that names no country at all is still refused rather than guessed at, with the row now listing every form that works, and a country is never swapped for a different one: the reader can only ever answer with a market in its own ISO 3166-1 table. Two-letter codes behave exactly as before.
- **A transcript is no longer started when the run cannot finish it.** With "Add the ad's transcript" on, a video ad's transcript is a download, an audio extract and a transcription, one after the other. The check that decided whether to start one counted the download and the transcription and left out the extract in the middle, so a transcript begun near the end of a run could download the video, spend the processing, and reach the transcription with no time left. All three steps are counted now. An ad whose transcript the run has no time for reads `skipped_deadline` in `transcriptStatus` — a new value, listed in the output description beside the others — instead of `failed`, because nothing about that ad failed and re-running the same request the same way would not help; raise the run's time limit or ask for fewer ads. The ad row itself is delivered and charged exactly as it always was, and nothing is charged for the transcript that was not started. Runs with time to spare are completely unchanged. No input field, charged event or price changed.

### 1.0.8 — 2026-09-15

- **A run that reaches its time limit now stops inside it, with the receipt you paid for.** Every download, Ad Library lookup and transcription carried only its own timer — up to three minutes for one transcription — measured from the moment that read started, so an ad picked up near the end of a run could still be fetching or transcribing when the platform cut the run off. When that happened the run lost its closing summary, its OUTPUT and its final status line for work that had really been done, which is the part you could not get back. Each read is now bounded by what the run itself has left as well, keeping about half a minute back to write the last rows and close the books; a read there is no longer room for is still attempted, just briefly, and still reports honestly and uncharged if it does not land. Nothing about your input, your columns, the prices or what gets charged changed, and a run with time to spare keeps exactly the timings it always had.

### 1.0.7 — 2026-09-13

- **A field name this actor does not have is now reported as your field name, not as ours.** When a run arrived carrying only a name this actor neither has on its form nor reads as an alternative spelling, the uncharged row explaining it was filed internally against "Search keywords or advertisers" — a box you had never typed in. It is now filed against the name you actually sent, the run page names that name beside the two this actor reads, and the row itself is unchanged except that it is clearer. A keyword or an ad link this actor genuinely could not use still reports against the field you put it in, because that one really is about that field. Nothing about pricing, the form, the output columns or what gets charged changed, and no run behaves differently.

### 1.0.6 — 2026-09-13

- **Keywords and ad links sent under a slightly different field name are now read instead of ignored.** If your call put them under a name this actor does not have on its form — `adUrl`, `adLink`, `url`, `urls`, `startUrl`, `adLibraryUrl` for the links, or `keyword`, `keywords`, `query`, `queries`, `searchQuery`, `searchTerm`, `advertiser`, `pageName` for the search — the run had nothing to look up and answered with its built-in sample search, which reads as if your call had worked. Those names are now read as the field this actor does use, your input is searched exactly as if you had typed it into the right box, and one uncharged row tells you the name to use next time. An ad sent under two names is still one ad and one result fee. If a value cannot be used at all, the row now names the field you put it in as well as the one this actor reads. Nothing about pricing, the form itself, or the output columns changed, and a field belonging to our transcript actor still earns the same hand-off row it always did.

### 1.0.5 — 2026-09-13

- **The store card now says what a run costs, and the page shows you a row before you spend anything.** The listing's description carried no price at all — it now says *From $2.00 per 1,000 ads* (the Business-plan floor, the same figure the page and the pricing tab have always carried) and that transcripts are an opt-in add-on. A new **What a row looks like** section shows one real delivered row from a live `fitness app` search, field by field. No price, no input, no output column and no charged event changed.
- **The form now tells an AI agent what a run costs before it calls this actor.** Apify's MCP server hands an agent the input form and never this page, so the description at the top of the form was the one screen that had to carry the whole story and did not. It now opens with the call that works, names what one ad row costs on the Apify free plan and on a paid one with the arithmetic for a thousand ads, names the separate transcript charge that **Add the ad's transcript** turns on and the long-video surcharge that rides with it, lists what is never charged, names the run option that caps the bill, and says where the one-pass Ad Library transcript actor belongs instead.
- **Search keywords** now leads with the value shape and says in its second sentence that a keyword matching nothing is never charged; everything it said before is still there, in a different order, with the "leave it empty and press Start" instruction at the end.

Your inputs, your columns, your events and your prices are unchanged, and nothing you have already collected needs re-running.

### 1.0.4 — 2026-09-12

- **An aborted or migrated run now leaves its receipt from the first moment of the run, instead of only after start-up finishes.** The record that says how a run ended was only put in place once the run had finished starting up — reading your input, opening its stores, working out what a previous attempt had already delivered and charged. A run stopped inside that window ended with nothing written about it at all, which mattered most on a re-started run, where the thing not written about was the earlier attempt's work. It is now in place from the run's first moment. Nothing else moved: no input field, output column, event or price changed, and a run that reaches its work behaves exactly as before.

### 1.0.3 — 2026-09-12

- The two caps have limits — 500 ads per keyword, 10,000 per run — and asking for a bigger number never stops the run. The run goes ahead at the limit and the dataset carries one uncharged row saying what you asked for and what the run used.
- A search that ends on one of the run's OWN limits says so and names the lever: pages that stopped carrying ads new to the run (usually ads another of your keywords had already listed) and the per-search page bound each get their own uncharged note row — never the sentence about the ad format you asked for, which is now reserved for a search that really did return only other formats. A search cut short this way after listing some ads is no longer silent.
- **A search your run's own backstop stopped now says so in the run summary too, not just on the row.** Last build the uncharged row learned to say that stopping on pages of ads this run had already listed is a limit on the run, not a verdict about the advertiser — but the summary still filed it under "the search matched nothing", so the row and the summary said opposite things about the same keyword. The summary now names it separately, as a stop rather than a miss: nothing ran out, nothing failed, and nothing was charged. "No results" from now on means only what it says — the Ad Library ran the search and had nothing for it.
- The output tab's charging contract names that stop, so every status a row can carry is still listed there.

Your inputs, your columns, your events and your prices are unchanged, and nothing you have already collected needs re-running.

### 1.0.2 — 2026-09-12

First public build.

- Meta Ad Library search by keyword, advertiser page name or pasted Ad Library link, scoped by country, ad format and running status.
- One structured row per ad: advertiser and page ID, flight dates and live status, publisher platforms and countries, creative format and media URL, ad body text, headline, caption, link description and landing page, call-to-action, Meta's own classification (categories, gated type, sensitive-content and AI-media flags, disclaimer label), the collation a repeated creative belongs to, and the EU spend, impressions and reach bands.
- `startedAfter` / `startedBefore` date filters, applied to what the search returns and never charged for the ads they drop.
- `maxAds` also answers to `maxItems`, `resultsLimit`, `limit` and `count` — the names other Ad Library scrapers use. The smallest limit present binds, and one uncharged note row says which name was read and what it was read as.
- Opt-in `includeTranscript`: video ads are transcribed onto the same row (`transcript`, `hook3s`, `language`, `durationSeconds`, `segments`), charged only when a transcript is delivered.
- Never charged: a search that matched nothing, a walled or rate-limited fetch, a link that would not resolve, an ad whose row could not be read, ads dropped by your own filters or cap, and — on the transcript leg — silent ads, image ads, expired video links and failed transcriptions.
- A cost cap or a time limit ends the collecting, never the delivering: rows already in hand ship in full, uncharged if they could not be billed, and say so.
- Store copy says what a run can reach and what it cannot: the per-keyword and per-run caps, Meta's own depth limit on a broad search, why `spend`, `impressionsText` and `reachEstimate` are `null` outside EU and political ads, that the column names are ours and do not move with Meta's, that a single ad's own link is a first-class input, and that there is no sort control.
