- The run's OUTPUT record now counts the keywords the Apify free plan's daily allowance held apart, as
freePlanHeldUnits, next to skippedUnits, which now counts only the keywords a limit you set, the time limit or the run's own limit left out. Before, both were one number.
- On the Apify free plan, a keyword held back by the daily fresh-lookup allowance is now marked
retryable: true. The allowance comes back at a known instant, already given in the row's retryAfter, so a program can wait for it and run the keyword again then. Before, the row said retryable: false.
- The run page and the summary row now say how many keywords the daily allowance held back, for example "the 12 keywords not looked up return at 00:00 UTC (2026-10-25T00:00:00Z), or run now on Bronze". Before, the count was only in the rows and in
resumeCursor.
- When your account's own daily allowance is used, each held row now says to stop a batch of runs until the reset: every run started before then is held the same way. It is not said when another run of the same keyword list used the day, because a different list can still run.
- Nothing changed in the price, which keywords are looked up or charged, the input fields or the dataset rows.
- On the Apify free plan, a keyword held back by the daily fresh-lookup allowance now names the plan that runs it at once, and the exact moment it comes back. The row, the run page and the summary row now say the rest returns at 00:00 UTC, with the instant itself (for example 2026-10-25T00:00:00Z), or runs now on Bronze, Apify's first paid plan (Starter), which has no daily lookup limit. The held row also says what a keyword result costs on Bronze. Before, they said only "any paid Apify plan".
- The same two facts now come as fields a program can read. A held row, and the summary row of a run the daily allowance stopped, carry
reason: "plan_limit_reached" and retryAfter (the ISO instant the allowance resets), next to the sentence. The run's OUTPUT record carries them too.
- On the Apify free plan, a run your maximum cost per run stopped after it used the day's fresh lookup now says that a re-run today is held, and when the rest comes back, instead of suggesting a new cap that a same-day run could not use.
- A run that asks for keyword ideas and meets the daily allowance now says so on a row. One uncharged row names the seed keywords that were not expanded and when they come back. Before, only the run page said it.
- Every row now has a
status and a chargedEvents column, and the summary row a status of run_summary. status is one word for what happened to that keyword (delivered, nothing_found, not_accepted, try_again, limit_reached, plan_limit_reached, or info for a note), and chargedEvents is exactly what that row was charged. The run page also names the move for a refused keyword (edit it and send it again) and, after a time limit, says to send the keywords left in resumeCursor in a new run.
- Nothing changed in the price, which keywords are looked up or charged, or the input fields. The existing output columns are unchanged.
- On the Apify free plan, a handed-back lookup is now honoured by the very next run, and the row only promises it once it is granted. When a run's fresh lookup finds no Google Ads data for any keyword, the day's lookup is handed back once a day. The next run now checks the hand-back on its own record, so it gets its fresh lookup even seconds later. The row that says "you can run one more fresh lookup today" now appears only after the hand-back is written and read back; if it could not be written, the row is not shown and the day stays used.
- Only a lookup that covered your whole list is handed back. A run that your maximum cost per run, your time limit or your row limit stopped before every keyword was looked up keeps its lookup counted, even when the keywords it did reach had no data.
- Nothing changed in the price, which keywords are charged, the input fields or the output columns.
- Rebuilt on the patched Node runtime (no code change).
- On the Apify free plan, a keyword held back by the daily fresh-lookup allowance now says when it comes back. The row, the run page and the run log now say the rest returns at 00:00 UTC, when the next day's allowance starts, and that any paid Apify plan has no daily lookup limit. Before, they said "tomorrow", which could be minutes or a whole day away depending on where you are.
- Nothing changed in the price, which keywords are looked up or charged, the input fields or the output columns.
- A small run now says that the fresh-lookup fee covers the whole run. When a run of one to five keywords pays the fresh-lookup fee, the run page and the summary row now say that the fee covers a whole run, so one run with all your keywords pays it once. Before, this was only said when most of a small list came back with no data. It is not shown on a run answered from the 30-day cache, on the sample run, or on a run that asks for keyword ideas, where one run of all your seed keywords returns one combined set of ideas.
- Nothing changed in the price, which keywords are looked up or charged, the input fields or the output columns.
- A run stopped by your maximum cost per run now tells you what to set. When
maxTotalChargeUsd (the run option "Maximum cost per run") stops a run before your list is done, the run page and the summary row now name your cap, how many keywords it covered at this run's price per keyword, how many are left, and one figure that covers the whole list — for example: "stopped at your maximum total charge of $0.49 after 26 keywords at $0.011 each plus the $0.19 fresh-lookup fee — 4 of 30 keywords left, set it to about $0.55 to cover all 30". Before, it only said to raise it.
- When your account memory is on and your cap already covers the rest, the line says you can simply run it again. The keywords already delivered come back uncharged, so the same cap finishes the list. On the Apify free plan this is not offered, because a second fresh lookup the same day is held for the next day.
- The README now says what a
0.50 cap buys on each plan: with a fresh lookup, about 25 keywords on the Apify free plan, 27 on Bronze, 62 on Silver and 77 from Gold.
- Nothing changed in the price, which keywords are looked up or charged, the input fields or the output columns.
- Large runs hand over their rows much faster. A run now saves what it delivered to your account's memory once per page of up to 1,000 rows instead of once per row, so a run of a few thousand keyword ideas, and several runs of one account going at the same time, no longer spend most of their time on it. Every keyword is still saved to your account before it is charged, so a keyword your account already got still comes back uncharged on your next run.
- A run that delivered everything you asked for no longer says it stopped at your run-time limit. When a run finishes handing over rows it had already fetched after maxRunSeconds has passed, the run page now reads as complete, with no advice to raise maxRunSeconds. A run whose time limit really did leave keywords out still says so and lists them in resumeCursor.
- Nothing changed in the price, which keywords are charged, the input fields or the output columns.
- The new prices are in effect from 17 October 2026. A keyword result now costs $0.012 on the Apify free plan, $0.011 on Bronze, $0.005 on Silver and $0.004 on Gold and above — from $4.00 per 1,000 keyword results. The price still falls with your Apify plan, and the $0.19 fresh-lookup fee and the $0.25 minimum for a run’s maximum cost are unchanged. The README, the input form and the run page quote these prices.
- On the Apify free plan, a lookup that finds no data for any keyword no longer uses up your day. If every keyword in a fresh lookup comes back with no Google Ads data — often the wrong country or language — the day's fresh lookup is handed back once, so a corrected list can run the same day. One uncharged row and the run page say so. A second lookup with no data on the same day still counts, as does any lookup that returned figures for at least one keyword.
- Two of your runs going at the same time no longer charge the same keyword twice. Right before charging, a run checks your account once more, and a keyword another of your runs has just delivered comes back marked
repeat: true and not charged.
- A website pasted as a keyword now points you to the right tool.
example.com or https://example.com/page is still looked up and charged like any keyword. One uncharged row and the run page now name steadyfetch/competitor-keywords-scraper for the keywords a website already ranks for.
- A keyword with no published volume now says more about why. Its row also names the market as a likely cause (the phrase may be searched mostly in another country or language) and the move that goes with it. It also names the two sample keywords that always return figures, so you can tell a gap in your phrases from a problem with the run.
- A keyword-ideas run's summary row now counts ideas. The last row in the dataset now reads "350 keyword ideas delivered from 3 seed keywords", the same words as the run page. A run with both modes names the keyword rows and the ideas apart.
- Plainer words on two rows. A rate-limited batch and a lookup that failed on our side now say what happened in plain words. Both rows stay uncharged and can be run again, exactly as before.
- Nothing else changed in which keywords are looked up or how they are charged, and the input fields and output columns are the same.
- The platform's own ways of naming a market and a keyword now work. A country sent as a location number (for example 2826) is now read as that market instead of the default. The keyword source's and Google's own field names (
location_code, gl, location, language_code, hl, …) are read as Country and Language. A Google search, Trends or News link pasted into Keywords is read as the words searched for, with one uncharged note row saying how it was read. Nothing changes about the price or the output columns.
- New: search volume for a keyword list from one web request. Besides running the actor as before, you can now send your keywords to the actor's endpoint — GET or POST to the address on its Endpoints tab, with
keywords and optionally country, language and network — and get Google Ads monthly search volume, CPC and competition back as JSON in the same response, with no run to start and no dataset to read. Each row has the same fields as a row in a run's dataset, and keywords Google Ads refuses or has no data for get the same uncharged rows a run writes. You are charged exactly as in a run: per keyword returned with data, plus the one fresh-lookup fee when a request needs new data, and no fee when every keyword comes from the 30-day cache. If the charge for a request cannot be completed, its keywords are not sent and nothing is charged. One request takes up to 1,000 keywords, and 100 on the Apify free plan, where the endpoint also has a shared hourly and daily limit on new lookups. The endpoint returns search volume only (keyword ideas stay with runs), and it does not hand back keywords your account already looked up: every request is answered and charged on its own.
- Running the actor works exactly as before: nothing changes about the price, which rows are charged, the input fields or the output columns.
- A keyword with a currency sign or another symbol Google Ads refuses no longer sinks the rest of your list. Google Ads refuses a whole batch of keywords when one of them carries certain characters. This actor already caught commas, percent signs, parentheses and similar marks before the lookup; it now catches the full set Google Ads refuses, including currency signs such as ₹, curly quotes and apostrophes, long dashes, math symbols and invisible characters. Each such keyword gets its own uncharged row naming the character, and the rest of your list is looked up as normal.
- When Google Ads refuses a batch for a reason no character rule can see, the run now finds the one keyword and delivers the rest, on every Apify plan. It splits the batch until it finds the keyword Google Ads refuses, then looks up all the others together. Before, a run on the Apify free plan stopped at the first refusal and returned none of that batch, and a run on any plan could return only part of it when the refused keyword sat early in the list. The refused keyword's row names the character that caused it when there is one.
- On the Apify free plan, a lookup Google Ads refused no longer counts as your fresh lookup for the day. Only a lookup Google Ads actually answers counts, so running the same list again right away works.
- When a run cannot find which keyword caused a refusal, the row now says why and what to do: the run reached its limit first, so remove special characters such as currency signs, curly quotes and other symbols, or send fewer keywords at a time, and run it again.
- Nothing about the price, the input fields or the output columns changed. Keywords are charged exactly as before, and a refused keyword is never charged.
- A run on the lowest memory setting no longer stops at its very end. A run for a busy market, most often the United States, could run out of memory on the lowest memory setting this actor accepts after its keyword rows were already delivered and charged, and end as failed with no summary row and no line on the run page. It now finishes and writes both on every memory setting, and a busy-market run uses far less memory on every setting. Nothing about which keywords are looked up, delivered or charged changed, and the price, the input fields and the output columns are unchanged.
- Bookkeeping only: a run whose Language is set to any language is now counted as such in this actor's own run statistics, so we can see how often that reading is used. Nothing about which keywords are looked up, delivered or charged changed, and the run page, the dataset rows, the input fields and the output columns are unchanged.
- Language "any" now runs with no language filter instead of being refused. "any", "auto", "all", "any language", "auto-detect", "automatic", "none" and "*" in Language used to stop the whole run with a note asking for a language. They now look your keywords up with no language filter, so the figures count searches in every language for the country you set. One uncharged note row and one short clause on the run page name the language to set next time if you want to narrow it, for example German (de) or English (en) for Germany. These figures are cached and remembered apart from any single language's, so a run in English never returns them and they never replace English figures. Keywords are charged exactly as in the same run with a language set.
- A keyword-ideas run now counts ideas against your seed keywords on the run page. It used to read "Delivered 350 keyword rows from 3 requested keywords"; it now reads "Delivered 350 keyword ideas from 3 seed keywords". A run that returns both your keywords' figures and ideas names the two counts apart: "Delivered 3 keyword rows from 3 requested keywords and 5 keyword ideas". A run for your own keywords only reads exactly as before.
- Apart from the Language reading above, nothing about which keywords are looked up, delivered or charged changed, and the input fields and output columns are unchanged.
- A run where most keywords delivered and a few had no published volume no longer asks you to open an issue. Google publishes no search volume for some phrases, and a re-run gives the same answer, so the run page now reports those phrases with the move that works (try the head term or a nearby city) and stops there. A run where more than half of the keywords came back empty still carries the Issues-tab line, because there the country or language is a likely cause.
- A keyword-ideas run that lists fewer ideas than the keywords you sent no longer reads as a problem. The ideas for all your keywords come from one lookup and its number is Google's. Keywords the run did not expand are still reported on their own row.
- "What to return" now reads more of the ways it gets written. "search volume", "keyword metrics", "related keywords", "keyword suggestions", "metrics and ideas" and similar now run as metrics, ideas or both instead of falling back to metrics with a note row.
- A keyword list sent as JSON text is read as that list.
"[\"seo tools\", \"crm\"]" in "Keyword list (paste)", or as the whole keywords value, now looks up "seo tools" and "crm" instead of two phrases carrying brackets and quote marks.
- Apart from the two input readings above, nothing about which keywords are looked up, delivered or charged changed, and the input fields and output columns are unchanged.
- A run where every keyword comes back with no volume, fast, now names the likely cause. When every keyword in a run comes back with no published volume and the answer arrives in under 5 seconds, that usually means the language and country pair has no data at all — for example Japanese searches in Germany — rather than that nobody searches your phrases. The run now adds one uncharged note row and says so on the run page, naming the pair you sent and the languages to try for that country: its own main language, or English. A run where only some keywords are empty, a run with one keyword, a slower answer, or a run already in that country's own language or in English reads exactly as before.
- A small, mostly-empty run now says how to pay the per-run lookup less often. When a run of 5 keywords or fewer paid the per-run lookup and more than half of its keywords came back with no volume, the run page now adds one sentence: put all your keywords in one run, because the per-run lookup is paid once per run however many keywords it carries. It never appears on a run answered from the 30-day cache, where no lookup is paid.
- Nothing about which keywords are looked up, delivered or charged changed, and the price, the input fields and the output columns are unchanged.
- A run that runs out of time now names the limit that actually stopped it. Until now every run ended by the clock said "raise maxRunSeconds", even when the run's own Timeout (Run options, or
timeout on an API call) ended it first, or when it reached this actor's 30-minute limit per run — and in both cases raising "Max run seconds" changes nothing. Now a run stopped by your "Max run seconds" says to raise maxRunSeconds; one stopped by the run's Timeout says to raise that Timeout; one stopped by both at once says to raise both; and one that reached the 30-minute limit says to continue in a new run, with the keywords it did not reach listed in resumeCursor as before. Nothing about which keywords are looked up, delivered or charged changed, and the price, the input fields and the output columns are unchanged.
- The input description now says what an empty input does. Sending
{} runs a two-keyword sample in the US market, charged like any run; the description an AI agent reads now says so before it calls.
- The Language field now reads a language written in its own name, or with a region attached. "Español", "Deutsch", "Français", "Português", "日本語", "한국어", "العربية" and the other languages' own names now run as that language, and so do "English (US)", "American English", "Spanish (Spain)" or "Português (Brasil)": the region part is set aside, because the Country field already picks the market. Codes, three-letter codes, tags like "en-US" and English names work as before. A value that names more than one language, or none, is still refused on an uncharged row with nothing looked up. A run with a language written any of these ways is charged exactly what the same run with the plain code is charged.
- "zh-HK", "zh-Hant" and "Chinese (Taiwan)" now look up Traditional Chinese. Before, a Hong Kong or Traditional tag was read as Simplified Chinese, so its volumes were for the other script. "zh-CN", "zh-TW" and the Chinese entries in the list are unchanged.
- The page title now reads "Keyword Search Volume Scraper — Google Ads Keyword Planner, CPC". Same actor, same id, same URL, same input and same output: only the words on the listing changed, so nothing you have saved, scheduled or wired into an integration needs touching. The title now names what buyers search for.
- From the 17 October price change, the run page keeps naming the Apify free-plan column with the right prices. A run on the Apify free plan ends with one line saying which column of the price table billed it and what the same run costs on Bronze, Silver and Gold. That line only knew today's prices, so from 17 October it would have quietly disappeared from every free-plan run. A run now reads the price table in force when it started: before 17 October it quotes today's prices, from then on the new ones. Nothing about which keywords are looked up, delivered or charged changed, and the price, the input fields and the output columns are unchanged.
- Price change from 17 October 2026 (16:57 UTC). From then a keyword result costs $12.00 per 1,000 on the Apify free plan, $11.00 on Bronze, $5.00 on Silver and $4.00 on Gold and above (today: $8.00, $5.50, $3.50 and $2.00). The fresh lookup stays $0.19 once per run, runs answered from the 30-day cache still pay no lookup fee, a keyword with no data is still never charged, and the Apify free plan's one fresh lookup per account per day carries on at the new price. Until 17 October nothing changes, and the README carries the dated notice beside today's prices.
- The note a run writes when "Max run seconds" is outside its range no longer says a run can last an hour. Ask for less than 30 seconds or more than an hour and the run still starts on the nearest end of the range with one uncharged note row; that row used to say a run of this actor lasts up to an hour, but every run ends by 30 minutes. It now says so. Nothing about which keywords are looked up, delivered or charged changed.
- A country we cannot place is now refused before anything is looked up, and nothing is charged. Until now a Country value we could not match — a typo like "XX", a made-up name, a half-typed name like "Germ", or a Google Ads location code for a city or region — ran your whole list on the United States market and charged for it, with a note row saying so. Now nothing is looked up: every keyword gets an uncharged row naming the value you sent and the values that work, so you can fix it and re-run on the market you meant. Every country we do place — by code, name, name in its own language, locale tag, list, Google Ads country code, or the
locationName / languageName fields — runs and is charged exactly as before.
- The run page no longer says Google Ads rejected keywords it was never sent. A keyword this actor refuses before any lookup — one made only of symbols, an emoji, one that is too long or one with a character Google Ads does not accept — was counted on the run page as "rejected by Google Ads", although it was never sent. Those keywords now read "not accepted", and only keywords Google Ads itself refused keep that wording. Each refused keyword still gets its own uncharged row naming the reason. Nothing about which keywords are looked up, delivered or charged changed, and the price, the input fields and the output columns are unchanged.
- The shared 30-day cache is now kept in a few records per run instead of one per keyword. A long keyword list used to save each answer one by one; it now saves them together when the run closes. Nothing you see changed: a keyword already in the cache for the same country, language and network is still served from it (
servedFromCache: true) with the same figures, it is still charged like any other keyword, and a run answered entirely from the cache still pays no fresh-lookup fee. Your own memory of keywords you already got is untouched, and the price, the input fields and the output columns are unchanged.
- A keyword made only of symbols is no longer looked up or charged. A keyword with no letter or digit in it, such as
$$$, ..., → or ★★, used to be sent with the rest of your list. In "ideas" mode it was expanded together with your real keywords, and the unrelated ideas it brought back were charged like any others. Now it gets its own uncharged row with missReason: INVALID_KEYWORD that names the keyword, and only your real keywords are looked up. Keywords in any language, keywords with digits such as iphone 15, and ordinary punctuation such as men's shoes or c++ tutorial run exactly as before.
- A keyword your account already got is no longer charged twice when several runs finish together. When several of your runs on one account finished at the same moment, a keyword one run had just remembered could be forgotten and charged again by a later run. The memory now checks that its last write landed and puts it back if it did not. Nothing else about what is charged changed, and the price, the input fields and the output columns are unchanged.
- The run summary no longer lists internal cost telemetry. The run's OUTPUT record carried two figures about our own data costs that were never meant for buyers; they are gone. Your keyword rows, the counts in the summary, the status line, the charged events and the price are all unchanged.
- The input description now says how big one run can be. One run delivers up to "Max keyword rows for the whole run" (
maxItems) rows, 1,000 by default and up to 100,000, and works for up to "Max run seconds" (maxRunSeconds), 900 by default. Keywords a run does not reach are not looked up or charged, and the summary row's resumeCursor lists them, so send those in the next run. Running the whole list again hands back the keywords your account already got for the same market in the last 30 days uncharged, and those rows still count toward maxItems. That memory needs the run to be able to read your account's storage; a restricted API token can block it, and the status line then says so. The "Keywords" field says the same in its first sentences.
- "Max run seconds" now says plainly that a run never works past 30 minutes. It still accepts anything from 30 seconds to an hour, but this actor ends every run by 30 minutes, so a value above 1800 runs as 30 minutes. The field and the README said "up to an hour" without that. Nothing about which keywords are looked up, delivered or charged changed, and the price, the input fields and the output columns are unchanged.
- A finished run now tells you how to track the same keyword list every month. When a run delivered keyword rows and ended cleanly, its status line adds one sentence: save the list as a task on a monthly Apify Schedule — Google refreshes these figures once a month, and a keyword your account already got for the same market in the last 30 days comes back uncharged. It only appears when there is room on the line, never on a run a Schedule started, and never when the run could not open your account's memory. The input description an AI agent reads now says the same near its top. No price, charged event, input field or output column changed.
- A run cut off before it finishes is now accounted for. The run now notes its own start in its internal record, so a run that ends without finishing — 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.
- An input that names its market as
locationName and languageName now runs in that market. Some keyword tools and APIs call their country and language fields locationName and languageName, and an input copied from one of them used to run in the United States in English, with the two names listed as ignored on the note row. From this build locationName is read as Country and languageName as Language, and both go through exactly the same checks as a value typed into those fields, so "Germany" and "German" work as they do there. If you also filled this actor's own Country or Language, yours is used and the pasted value is set aside. The uncharged note row says which names were read as which field and which were set aside. No price, input field, output column or charged event changes.
- A run held back by the Apify free plan's daily fresh-lookup allowance no longer asks you to open an issue. When your account's fresh lookup for the day is already used, or the same keyword list already had today's fresh lookups on other accounts, the run serves what our 30-day cache holds, ships one uncharged row per keyword it held back, and says so on the run page. That is the allowance working as this page describes it, but the run page still ended with "Something off? Open an issue", as if something had broken. It no longer does. Anything else that goes wrong on the same run, such as a keyword with no published volume, the source refusing or rate-limiting a lookup, the run clock or your own cost cap, still ends with the ask exactly as before. The held rows, their wording, the price, the input fields, the output columns and the charged events are all unchanged.
- Settings sent wrapped in an extra "input" object are now read. A run whose input arrives as
{"input": {…}} — for example from run_input={"input": {...}} in the Python client — now reads those settings as if they were sent directly and adds one uncharged note row naming them. Before, the run stopped with a guidance row and looked up nothing. Nothing changes about the price, the input fields or the output columns.
- 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.
- An input copied from Google Search Results Scraper now runs. That actor's
queries box holds one search per line, and this actor used to read the whole box as a single keyword — so three lines came back as one long keyword nobody typed — and it ignored countryCode and languageCode, so the run quietly used the United States in English. From this build queries is read one keyword per line (a list of strings works too), countryCode is read as Country and languageCode as Language, and both go through exactly the same checks as a value typed into those fields. If you also filled this actor's own Keywords, Country or Language, yours is used and the pasted value is set aside. The uncharged note row says which names were read as which field, which were set aside, and which were ignored (maxPagesPerQuery, resultsPerPage and the rest have no meaning here). A keyword with a character Google Ads refuses is still refused on its own row before anything is looked up. No price, input field, output column or charged event changes.
- The Keywords field is no longer marked required, and its help now opens with what to pass. The field has always carried a built-in sample value, so an API call or an AI agent that left it out got the two-keyword sample — the same thing a bare Start in the console does. Apify's own input-schema guidance says a field with a built-in value should not also be marked required, so it is not any more; what tells a caller what to send is the field's first sentence, which already names the shape with an example (
["project management software", "crm for startups"]). Nothing a run does changes: leave the field out and you still get the sample, and a Keywords value sent as null is still refused before the run starts, exactly as before.
- The top of this page now says how often the data changes and how to schedule it. Google refreshes Keyword Planner volumes once a month, so the same keyword asked again inside 30 days is the same figures, handed back with nothing charged. The first screen now says so, and says how to put your input on an Apify Schedule once a month — each run looks up and charges only the keywords your account has not had in the last 30 days. The full section further down is unchanged. No price, input field, output column or charged event changes.
- A failed internal bookkeeping write no longer prints its storage address in the run log. Nothing changes about what is delivered, which rows are charged, the price, the input fields or the output columns.
- The run page now names the pricing column that billed your run, and what the same run costs on the other Apify plans. What this actor charges for a keyword result depends on the Apify plan your account is on, and the store page has carried the whole four-rung table since the price was filed — but the run page, the one screen you are actually looking at while a run spends your credit, never said which column you were on, so there was no way to check a charge against a ladder nobody had told you you were reading. From this build a run billed at the Apify free plan's column says so in its own line: $8.00 per 1,000 keyword results there, against $5.50 on Bronze, $3.50 on Silver and $2.00 from Gold. A run on a paid plan reads exactly as it did before — the sentence appears only where it is true, and it is read from the price your own run was actually charged at, never guessed from your account. Nothing about what anyone pays changes: the per-keyword ladder, the $0.19 fresh-lookup fee and the two cases that waive it, the daily fresh-lookup allowance on the Apify free plan, the input fields, the output columns and the charged events are all exactly as they were, and every figure in the new sentence is a price that has been published since the ladder was filed. It is also the first thing given up if the line would otherwise run past the 500 characters the platform shows: what was delivered, what you already had, what was not charged, the cap that stopped the run and how to raise it all keep their place ahead of it.
- You can now type your country's name in your own language. Until this build the Country box understood two- and three-letter codes, the country's English name and the Google Ads location code — so "Germany" worked and "Deutschland" did not, and neither did "España", "日本", "Україна" or "مصر". A country the run cannot place stops the whole lookup, so one spelling in your own language cost you the entire run: every keyword came back on its own uncharged row saying the market could not be placed. From this build 47 common native spellings are understood and land on exactly the same market their English name does — German, Austrian, Swiss, Spanish, Italian, Portuguese, Dutch, Belgian, Polish, Nordic, Baltic, Czech, Slovak, Slovenian, Croatian, Serbian, Hungarian, Albanian, Bulgarian, Greek, Ukrainian, Japanese, Korean, Chinese (both scripts), Taiwanese, Hong Kong, Thai, Filipino, Indian, Arabic and Hebrew spellings among them. Accents, case, hyphens and underscores still do not matter, so "Türkiye", "România", "México" and "Perú" already worked and still do. Nothing became a guess: a value that names no market is still refused, uncharged, with a row saying what was sent and what works, and a market the source does not report on is still refused rather than quietly swapped for a neighbour. The Country field's help text, the refusal rows and the README now all say the native name is accepted. No price, input field, output column or charged event changes.
- On the Apify free plan, one keyword list is now looked up fresh for at most two accounts a day, whatever market each of them asks for. This actor looks search volume up in batches, and one batch costs the same whether one account asked for it or thirteen did. Until this build the daily fresh-lookup allowance was counted per account, so the same keyword list run by a crowd of free accounts bought the same fresh data over and over. From this build the allowance also counts the list: the first two free accounts to ask for a list on a given day buy fresh data as they always did, and past those two a free account's run is served from the 30-day cache instead — those rows are delivered and charged exactly as before, and every keyword that would have needed a brand-new lookup ships as its own uncharged row saying so, with
resumeCursor naming it for a later run and the run page saying the same. The list is read as a set, so the order you typed your keywords in, their capitalisation and a keyword repeated twice all make no difference, and it does not depend on the market: running the same list against another country is the same list. Two accounts that happen to share a list are both served in full, and an account re-running its own list is never held back by its own earlier runs. Paid plans are not affected in any way — neither allowance exists on them — and nothing about what anyone pays changed: the per-keyword price, the $0.19 fresh-lookup fee and the two cases that waive it, the input fields, the output columns and the charged events are all exactly as they were, and a keyword that was not looked up is still never charged.
- The run page's one line is now measured with its opening phrase included, so a long run no longer loses its last sentence to the platform's 500-character cut. Before this build the opening phrase of a sample run was added after the line had been sized, and on the longest runs the sentence that got cut off was the one saying which rows will be charged again next run. The run also records its own verdict (a problem or a clean run) as a plain fact instead of inferring it from the line.
- The run page's one line now always fits, so the stop, the repeat-memory warning and the charge statement can no longer be cut off. The platform shows the first 500 characters of a run's status line and drops the rest. On a run carrying a full list of what was not delivered beside a budget stop and the repeat-memory warning, this line ran to 598 characters, so the page cut it mid-sentence — and the half it cut was the half about your money. From this build the line has one more step to give way with: the per-reason breakdown of what was not delivered folds to its total, with "not charged, the rows name the reason" kept word for word in front of it and every count still on its own row and in OUTPUT. Nothing else on the line moves: what was delivered, what you already had, the cap that bound and the fix for it all stay. No input field, output column, charged event or price changed.
- Keyword ideas now come out of our 30-day cache, the way keyword volumes already did. Expanding seed keywords into new ones buys a fresh request, and until this build every expansion bought a fresh one — even when the same seed keywords had been expanded for the same market minutes earlier. From this build an expansion is kept for 30 days under the seed list together with the country, the language and the network, and a later run of those same seed keywords in that same market is answered from it: no fresh lookup is bought and no fresh-lookup fee is charged. Add a seed, drop a seed, or move the list to another country and it is a different expansion, so it buys fresh data — which is the same rule the keyword cache has always had, for the same reason: the source publishes one market at a time. Your rows are unchanged and so is what they cost: idea rows are charged at the same per-keyword price whether the expansion was bought fresh or served from the cache, because the cache saves the lookup, never the row. Rows served that way are marked
servedFromCache: true exactly as keyword rows are, and the run's summary row now says when your ideas came from the cache and what makes a second run of the same seeds cheap. The README's cache section says the same in full. No price, input field, output column or charged event changes.
- The run summary now says what our 30-day cache did for your run — and that the cache is per keyword AND market. Every row already carried
servedFromCache, but the summary row you read to decide whether to run again said nothing about the cache at all, so the one thing that makes a second run cheaper was invisible unless you opened the dataset and counted. From this build the summary says how many of your keyword rows came from the cache, that no fresh lookup was bought for them, and — where the whole run was answered that way — that no fresh-lookup fee was charged. It also says the thing that decides whether the cache can help you at all: Google publishes search volume one country at a time, so the cache is keyed on the keyword together with the country, the language and the network. seo tools in the United States and seo tools in Germany are two different answers and two different cache entries. Re-running the same list in the same market inside 30 days is what the cache answers; changing the country or the language makes it a fresh lookup and pays the fresh-lookup fee, which is why a list you have run before can still need new data when you move it to another market. Sweeping one list across several markets therefore buys one lookup per market the first time and is served from the cache on every later run of that market inside 30 days. The README's cache section says the same in full. Nothing about what you pay changed: the per-keyword price, the $0.19 fresh-lookup fee and the two cases that waive it, the daily fresh-lookup allowance on the Apify free plan, the input fields, the output columns and the charged events are all exactly as they were, and a keyword that is not looked up is still never charged.
- On the Apify free plan this actor now buys one fresh lookup per account per day, where it bought two. Keywords already in the 30-day cache are still delivered and charged exactly as they always were, and keywords that need fresh data come back the next day or on any paid Apify plan — the run page and every held-back row say so, and name the keywords left for the next run. Paid plans are untouched: nothing about a paid run's lookups, rows, prices or charges changes in any way. This build also closes a gap where two runs of the same free account starting at the same moment could each buy a fresh lookup where only one was allowed: the day's allowance is now taken in your account's own store before anything is bought, so two runs that start together share one allowance instead of two.
- A batch that is rate-limited now tells you what actually clears it. Lookups are answered at a fixed number per minute, shared across everything asking, so a rate-limited batch is a queue rather than a missing keyword — and starting again inside the same minute meets the same wall. Those rows used to say "please re-run"; they now say that a run a minute later answers. The rows are still uncharged and still marked retryable, and nothing about what you pay changes.
- The whole "Maximum cost per run" you set is now spent on keyword rows. Before this build the run held back about a tenth of your cost cap before it started — a cushion for platform running costs which, on this actor's pricing, your cap does not pay for at all: your cap buys keyword rows, and the running costs are ours. So the cushion was simply cap you had asked to spend and could not get. On a run capped at $0.10 with keyword rows at $0.002 the plan asked for 45 keyword rows where the cap paid for 50 — 5 keyword rows of your own cap, unspendable by construction. From this build the plan uses your cap in full, so a run asks for as many keyword rows as your cap can really pay for. Nothing you are charged changes, and nothing can be charged above the cap you typed: every charge is still checked against what is left of your cap at the moment it is posted, and a run that reaches your cap still stops cleanly, ships what it had already fetched, and tells you what is left. No price, input field or output column changes.
Internal accounting only — nothing changes in your rows, prices, charges, input fields or status line. When the Country box names a market the search-volume source carries no data for, or asks for a whole region rather than one country, our own run record now notes which of the two it was. It is a count, never the value you typed; your run, your rows and the wording you already see are exactly as before.
- The run summary now counts the keywords it refused as part of what you asked for. A keyword this actor refuses before it looks anything up — one over 80 characters, one over 10 words, or one carrying a character Google Ads does not accept — has always got its own uncharged row saying why. It was left out of the "requested" figure all the same, so a list of 81 keywords with 10 of them refused read "Delivered 31 keyword rows from 71 requested keywords" on the run page and in the summary row, while the dataset underneath it held 81 rows. The number now says 81: every keyword you sent is counted as asked for, whether it was delivered, came back with no data, was refused, or was left for a later run. That makes the summary reconcile against the dataset row for row — delivered plus every kind of miss equals requested — so the invoice and the row count can be checked against each other without arithmetic that never closed. Nothing else moved: the same keywords are refused for the same reasons with the same wording, the same rows are delivered, and the per-keyword price, the $0.19 fresh-lookup fee and the two cases that waive it, the input fields, the output columns and the charged events are all exactly as they were.
- One keyword Google Ads will not accept no longer takes the rest of your list down with it. Search volume is fetched for your whole list in one batch, and Google Ads refuses an entire batch when a single keyword in it carries a character it does not allow — so a list of 81 keywords with a dozen commas, percent signs, parentheses and a semicolon among them came back with nothing at all: every row said "Google Ads will not accept this keyword", including the 69 keywords Google Ads would have answered for. From this build those characters are caught before anything is bought. A keyword carrying one gets its own uncharged row naming the exact character it tripped on, and every other keyword in your list is looked up and delivered as normal. The characters Google Ads does not accept inside a keyword are the comma, exclamation mark, at sign, percent sign, caret, parentheses, asterisk, equals sign, braces, semicolon, tilde, backtick, angle brackets, question mark, backslash, vertical bar and horizontal bar, plus emoji and other 4-byte characters; full stops, hyphens, plus signs, ampersands, hashes, apostrophes, quotes, colons and slashes are all accepted and are looked up exactly as they always were. And when a batch is still refused for a reason none of those explains, the run now narrows it down instead of blaming every keyword in it: it splits the list, finds the keyword the source actually refuses, delivers the rest, and where it cannot narrow it down within a bounded number of tries the row says the batch was refused rather than naming your phrase as the cause. Refused rows also carry the source's own status code now, so which refusal happened is visible on the row itself. Nothing about what you pay changed: the per-keyword price, the $0.19 fresh-lookup fee and the two cases that waive it, the input fields, the output columns and the charged events are all exactly as they were, and a keyword that is not looked up is never charged.
-
The Country box now reads what people actually send, and a value it cannot place runs on the default market instead of refusing the whole list. Three spellings that used to be refused are read now, and each of them lands on a market the bundled country table already carries, so nothing is ever invented: a locale tag is read for its market half, since the language half is what the Language field already carries (en-GB is the United Kingdom, pt-BR is Brazil, and US-CA is still not Canada); a comma list runs on the first country it names, so germany, france and Dubai, United Arab Emirates both work; and the value we could not read before now says which market the run used. Where nothing at all can be placed, the run no longer returns you an empty dataset: it uses the default market, United States, and says so three times over — an uncharged first row naming both the value you sent and the market used, the same sentence on the run page, and the market itself in the country column of every delivered row. A bare number that is not a country code gets its own sentence, because it is not a typo: that is a Google Ads target for a place inside a country — a city, a region, a postal code — and this actor reports volume one whole country at a time, so the row tells you to send the country's own code or name. Two values are still refused outright, uncharged and one row per keyword, because a default there would be worse than nothing: a market the source carries no data for at all (returning a different country's numbers to someone who named a real one is the one thing this field must never do), and a whole-region ask like worldwide or EU, where picking a single country for you is exactly the substitution you did not ask for. A Country box longer than any country name is refused too — that is a pasted block, not a market. Nothing about what you pay changed with this: the per-keyword price, the fresh-lookup fee, the input fields, the output columns and the charged events are all as they were, the notice row is uncharged like every other notice, and a keyword that is not looked up is still never charged.
-
Fresh search-volume lookups now have a daily allowance on the Apify free plan. On a free Apify plan this actor buys at most 2 fresh lookups from the search-volume source per account per day. Everything you have already paid for is untouched by it: a keyword your account already had inside 30 days is still handed straight back uncharged, a keyword already in our own 30-day cache is still delivered and charged exactly as before, and the run still finishes successfully either way. Past the allowance a keyword that would have needed a brand-new lookup is simply not bought — it ships as its own uncharged row saying so, resumeCursor lists it so a later run picks it up where it stopped, and the run page says the allowance is used and that the rest comes back the next day or on any paid plan. If the run's API token cannot open your account's key-value store the daily count cannot be read at all, so that run keeps a single-run allowance of one fresh lookup and the rows and the run page say so too: a count we cannot read never becomes an unlimited allowance, and never becomes a refusal either. Paid plans are not affected in any way — the allowance does not exist on them — and a run whose plan this build cannot read is treated as a paid one. Nothing about what you pay changed here: the per-keyword price, the $0.19 fresh-lookup fee and the two cases that waive it, the input fields, the output columns and the charged events are all exactly as they were, and a keyword that was not looked up is never charged.
- The fresh-lookup fee is live, and every page now says so in the present tense. The README, the store listing and the input form had all announced it as a change still to come: a run that buys fresh search-volume data pays one fresh-lookup fee of $0.19, once per run whatever the size of the list. It is being charged now, so the wording that put it in the future is gone and each of those pages states the fee plainly where you decide to run it. Nothing about what you pay moved with this build: the per-keyword price is unchanged, the fee is still waived in full on a run answered entirely from the 30-day cache and on a fresh lookup that comes back with no data for any keyword, a keyword Google has no figures for still costs nothing, and the run option Maximum cost per run still needs $0.25 to cover one fresh lookup plus one keyword row. One more sentence on the input form was reworded for the same reason: with a second priced event, "one charged unit = one keyword" was an equality that no longer held, so it now says that each keyword that comes back with Google Ads data is charged once — which is what it always did.
- A country or language this actor cannot place now shows you the value you sent beside the values that work — on every row and on the run page. Only one row ever carried that: the field row at the top of the dataset named your value and listed the spellings that resolve. The rows underneath it — one per keyword, the ones a dataset opens on — said only that "the country you set could not be matched", naming neither what you typed nor what to type instead, while the run page closed by promising that "each row names the accepted values". From this build every one of those per-keyword rows carries your own value in quotes, in a new
input column beside the field name, and — where the value is simply one we cannot read — the accepted forms in full: a two-letter or three-letter country code, the country's English name, or its Google Ads location code. The run page names your value too, so a typo is visible without opening the dataset at all, and a value too long for the page is shortened rather than pushing the accounting off the end of the line. The two other answers keep their own wording: a country the source publishes no data for still says so and names the nearest markets that do, and a worldwide or region-wide ask still says volume is reported one country at a time. The Country field's own description now says the Google Ads location code works, that there is no worldwide figure, and that an unplaceable value refuses that field only. Nothing about money moved: the same keywords are refused, still uncharged, and no price, input field, output column or charged event changed.
- "Maximum run seconds" is now really the limit — a lookup already in flight can no longer run past it. The setting bounded when a new lookup could START; nothing bounded how long one could RUN. A request begun with nine seconds of a thirty-second limit left was still handed twenty seconds of transport, so a run could close more than a third past the time you set — and on every window the LAST lookup of a run was given the whole remaining clock, leaving none of it for the summary the run owes you. From this build each request to the search-volume source gets the smaller of its own timeout and the time your run actually holds above what the closing summary needs, and a lookup your remaining time cannot cover is not started at all: those keywords ship in
resumeCursor under "stopped before the run timeout — raise maxRunSeconds", uncharged, exactly as any other time stop already reads. Runs with time to spare are unchanged in every respect — the same requests, the same timeouts, the same rows — and no price, input field, output column or charged event changed.
-
A token that can READ your keyword memory but not WRITE it was quietly costing you full price on every next run — the run now says so, and says what to grant. The memory that stops you paying twice for the same keyword lives in a key-value store in your own Apify account, and an API token limited with "Restrict what Actors can access using the scope of this Actor" can carry key-value store Read without Write (or Write without Create on a store your account does not have yet). Such a run opened the memory, handed back every keyword you already had correctly — and then saved nothing, so your NEXT run bought the same Google figures all over again at full price, with no sign of it anywhere. From this build the very first refused write is named on the run page, in one uncharged row written the moment it happens, and in the run log beside Apify's own message, each saying to give the token key-value store Write (and Create) under Settings → API & Integrations or set Actor runs to Full access. A run whose memory saves normally is unchanged in every respect, a passing network hiccup on a write is still just a hiccup, and no price, input field, output column or charged event changed.
-
A run started with a scoped API token could be charged twice for the same keyword, and nothing told you why — it now says so, and says exactly what to grant. The check that hands back a keyword your account already paid for lives in a key-value store in your own Apify account (kw-volume-account). An API token limited with "Restrict what Actors can access using the scope of this Actor" cannot open that store unless it carries key-value store Read, Write and Create permission, so runs started from the API with such a token silently lost the check and paid full price again for figures Google had not changed — while the run page said only that it "could not check what you already had". From this build the run names the cause and the fix in three places: on the run page, in one uncharged note row written before any lookup (so an agent reading rows never misses it), and in the run log beside Apify's own message. Nothing changes for a run started from the console or with a full-access token, no price, input or delivered column changes, and every other reason the check can be unavailable keeps the wording it had.
-
A run that held keywords back under your cost cap no longer says it stopped at your maximum total charge when it charged you nothing. This actor reserves its charge slots out of maxTotalChargeUsd before it buys anything, so a cap can hold the last few keywords of a list back on a run that then charges $0.00 — every keyword it did look up came back with no published volume, or your account already had them. The run page used to answer that with "stopped at your maximum total charge — raise it", which reads as though the cap had been spent. It now says how many keywords were held back and which cap held them, for example "1 keyword held back under your $0.12 cap — raise it", and makes no charge claim. A run that really did charge up to your cap still says so, unchanged. The _summary row carries the same sentence and still lists the held keywords in resumeCursor so you can re-run just those. No price, no input field, no output column and no charged event changed.
-
A field name this actor does not read no longer disappears in silence. Send countryCode, locations or any other name the actor does not declare alongside real keywords and the platform passes it straight through — until this build the run looked perfect and quietly ignored it, so a caller (or an AI agent) had no way to learn the market, filter or limit they asked for was never applied. Runs now carry one uncharged row naming the fields you sent, saying they were ignored and nothing else about the run changed, and listing every field this actor does read with its form label. It refuses nothing and stops no keyword: the same rows are delivered and the same keywords charged as before, and a run that carries the row is still a clean run. A name the actor already understands as a keyword field — keyword, searchTerm, query and the rest — is read as before and is never reported here, and a run with no keywords keeps the answer it already gave, so nothing is said twice. No price, no input field, no output column and no charged event changed.
- A run that names the keywords field in the singular now looks up your keywords, instead of ours. If you — or an AI agent calling this actor — send
keyword, searchTerm, searchTerms, term, query or another obvious name for the same thing, those keywords are now read and looked up as though you had used this actor's own field name, keywords (and keywordText lands in the paste box, keywordsText). The run log says which name you sent and which field was read. Until this build the platform passed the unrecognised name straight through and the run treated your input as empty: it looked up the two example keywords and charged you for them, which is the wrong answer for someone who had named their own. If you set both names, everything you typed is kept and a keyword sent twice is still charged once. Sending one of those names with nothing in it now says so, naming the field you set and the field this actor reads, instead of running the sample. Nothing about money moved: no input field, output column, event or price changed, the form still shows Keywords, and the country, language, network and mode you set are read exactly as before.
- The page now shows a full delivered row, including the whole 12-month history. The Output section described the columns and never showed one; it now ends with a complete row from a live run — volume, CPC, competition, the bid range, every month of the trend and the charge record — so you can see the exact shape before you spend anything. No price, no input, no output column and no charged event changed.
- The store page now says how often these numbers actually change, and what a second run of the same list gets you. Google refreshes Keyword Planner volumes monthly, so a new section says that monthly is the cadence worth scheduling, that a weekly run of the same list inside the 30-day window is charged nothing for those keywords but also brings back nothing new, and how to set the schedule up: save the input as a Task, put the Task on an Apify Schedule, and add an integration or a webhook on run succeeded. It also names
dataAsOf as the field that tells you whether a run brought a new month of data. Nothing else moved: no input field, output column, event or price changed, and a run behaves exactly as before.
- A keyword you asked for ideas on, that the source has no ideas for, now comes back with a row saying so instead of an empty run. When you sent a keyword in "keyword ideas" mode and the source answered with nothing at all for it, the run finished with no rows about that keyword, nothing on the status line about it, and — the part that mattered — a run record that said you had asked for nothing. You now get one uncharged row per keyword saying the source answered and listed no ideas for it, that this is a gap in what Google publishes rather than an error, and that a broader head term is the thing to expand instead; the status line says the same in one clause, and the run's own record now carries the keyword you actually sent. Nothing was charged for those runs before and nothing is charged for them now — including the fresh-lookup fee that starts on 16 September 2026, which a run that delivers no rows never pays. That row also marks the keyword as completed, so a restarted run still never repeats an expansion you already paid for.
- On a run that could not read its own delivery record, the status line no longer says duplicates were "charged once" while the same line says nothing was charged. The two sentences contradicted each other; the charge statement is now made by whichever clause is entitled to make it, exactly as the rest of that line already worked. Nothing about what you are charged has changed. This also bought back the room the widest status line was over the run page's own 500-character cut with, measured at 502 characters on a 1,000-keyword run where every kind of outcome appeared at once — past that point the run page truncates, and what it truncates is the accounting and the support ask at the end.
- A run that was interrupted and restarted now reports the keywords its first attempt already delivered. An ideas run that was moved to a new machine part-way through reported the rows it had delivered but reported the ask behind them as zero. The rows, the charges and your data are unchanged — only the run's own summary of what was asked for was wrong, and it now matches what was delivered.
- 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.
- This actor has a new title on the store: Keyword Search Volume Scraper — Google Keyword Planner, CPC. Same actor, same id, same URL, same input, same output, same price — only the words on the listing changed, so nothing you have saved, scheduled or wired into an integration needs touching. The title now carries the phrase buyers actually search for, which is the only reason it moved.
- The input page now spells out, in one place, what to send and what a run costs. The description at the top of the input now opens with a working example call, says that one charged unit is one keyword that comes back with Google Ads data ($0.008 on the Apify free plan, from $0.002 on paid plans — about $4 / $1 for 500 keywords), repeats that a keyword Google has no volume for is never charged so a whole list is safe to send in one run, names the fresh-lookup fee that starts on 16 September 2026 and the maximum cost per run to set for it, and says plainly that
_demo is the sample switch rather than a keyword. The Keywords field now leads with the value shape and an example instead of the console sample instruction. This is the screen an AI agent reads before it calls this actor — it never sees the store page.
- One wording fix on the price line in the FAQ: it now says "the Apify free plan", which is the name Apify uses and the one the rest of this page already used. Your price, the fresh-lookup fee and every figure on the page are exactly as they were.
- A limit outside this actor's range no longer refuses to start the run. "Max keyword ideas per keyword", "Max keywords to expand", "Max keyword rows for the whole run" and "Max run seconds" each had their range enforced by the input form itself, so
maxItems: 200000, or an AI agent guessing maxRunSeconds: 10, did not start a run at all: you got a validation error, no rows, no status line and nothing to look at. The ranges are unchanged — up to 2,000 ideas per keyword, 20 keywords expanded in one lookup, 100,000 rows for a run, and a run window between 30 seconds and an hour. What changed is what an ask outside one of them does: the run now starts, uses the nearest end of the range, writes one uncharged row saying what you asked for and what bound it, and names it on the run's status line. Your defaults are unchanged, and nothing about what a run costs moved.
- Nothing else moved: no input field, output column, event or price changed.
- A maintenance build: nothing about your runs changes. The top of this actor's store page now carries the one-click MCP pin, this actor's id, the one required input field with a real example, and the field that caps what a run can spend. An AI agent reads only the first part of a listing, and until this build all four sat far enough down the page that one never reached them. Every word about price, input and output is unchanged.
- The private run-report this actor writes for our own support — counts only, never anything you typed — now also records whether a run that stopped at the row limit you set had actually filled it, and the per-event prices the run was billed at. Until now a run stopped by your own limit looked the same to us as one that simply ran out of keywords.
- Charges are unchanged. No price, event, output column or charge moved: only delivered keyword rows are charged, a phrase with no published volume is uncharged, a keyword the source did not answer for is uncharged, and a keyword this account already had is still handed back uncharged.
- The summary row no longer tells you to re-run with keywords that are not there. When a run stopped early — on the cost cap, the time limit or the task cap — the last row always closed by telling you to re-run with the remaining keywords to pick up where it left off. It said that even when nothing was left: if the stop landed just as the final keyword was answered, the same row's
resumeCursor was empty and the instruction pointed at nothing. That sentence now appears only when there really are keywords still to do, and it names resumeCursor, the field on that row which lists them. A run that finished everything is unchanged, and so is what you were charged.
- Two runs started at the same time no longer erase each other's memory. The account's repeat memory is now merged on every write, so an item one run delivered stays remembered, and a later re-run hands it back instead of charging it again.
- Patience is now priced. A refused lookup is waited out only for as long as the keyword results it could still recover are worth, at the price and the memory this run actually has. The first pause is shorter, and a run on a short time limit now gets patience it never got before instead of none at all. A run that has delivered nothing still gets one pause whatever its ask is worth.
- Charges are unchanged. No price, event, output column or charge moved: only delivered keyword rows are charged, a phrase with no published volume is uncharged, a keyword the source did not answer for is uncharged, and a keyword this account already had is still handed back uncharged.
- A "Max run seconds" of 30 now actually buys you a lookup. The setting offers anything from 30 seconds upward, but the run kept a flat 20 seconds back to write its summary — leaving ten working seconds against a lookup that measures about 17. The time kept back is now a quarter of the window you set, so a 30-second run has 22 working seconds and one real lookup fits. Nothing changes at 80 seconds or above, the 900-second default included: those runs keep the same 20 seconds they always did.
- A run can no longer overrun the time limit you set while it waits on a slow source. Every request to the search-volume source was given up to two minutes to answer, whatever your limit was, so a hanging request could run a 30-second run for two minutes. A request is now given no more time than your run has left, and never less than one measured lookup needs. A run with more than two minutes left is unaffected.
- A phrase with no published volume no longer tells you why. The row used to say Google "groups close variants under a broader term" — one reason stated as the reason. All we can actually see is that the figure came back blank, and there are at least three ways that happens: it may be grouped under a broader term, sit below what Google reports, or be something Google simply has nothing for. The row now says what we saw and offers the three, instead of picking one. The advice is unchanged, because it works under all three: try the head term, or a nearby larger city.
- A keyword the source answered about but returned nothing for no longer reads as an outage. When a lookup comes back successfully and simply does not carry one of your keywords — and the extra ask for it comes back short too — the row said "the search-volume source was unavailable". It answered; it just had nothing for that keyword. The row now says that. A request that really did fail still reports as unavailable, exactly as before.
- Charges are unchanged. No price, event, output column or charge moved: only delivered keyword rows are charged, a phrase with no published volume is uncharged, a keyword the source did not answer for is uncharged, and a keyword this account already had is still handed back uncharged.
- Keyword ideas now cost the same whether you expand one keyword or twenty. Your keywords used to be sent for expansion one at a time, as a separate paid lookup each, while the search-volume source accepts twenty keywords in a single lookup and charges for the lookup rather than the keywords in it. They are now expanded together in one lookup. What you receive does not get smaller for it: a live check confirmed the same suggestions come back either way, and the run's idea ceiling is now filled from one list sorted by search volume, so a keyword with few suggestions no longer strands its share of the ceiling. You get at least as many ideas as before, and often more.
- "Max keyword ideas per keyword" is now the run's ceiling divided by the keywords you expand. Three keywords at 200 is up to 600 ideas for the run, taken from the highest-volume suggestions across all of them; it used to be applied to each keyword on its own. Your row cap and your maximum total charge still stop the run exactly where you set them.
- An idea row no longer names one keyword it came from. Google returns the suggestions as a single list and does not report which of your keywords produced which idea, so "From keyword" is now empty on an idea row and a new "Ideas expanded from" column lists the keywords it was expanded from. Nothing is guessed. "From keyword" is still empty on your own keyword rows, exactly as before.
- A run whose maximum total charge cut the keyword ideas short now says so instead of reporting as a completed run, and names the setting to raise.
- The receipt row that marks a keyword as expanded is written even when your row cap ends the run. Each keyword you expand gets one uncharged receipt row saying so. A run that filled its row cap with keywords this account already had could finish without them, leaving the expansion unrecorded in your dataset.
- That receipt row also counts the ideas you actually received. It counted only the ideas that were charged for, so a run answered entirely from keywords this account already had said the lookup returned none while the rows were sitting in the dataset.
- Charges are unchanged. No price, event or charge moved: only delivered keyword rows are charged, a phrase with no published volume is uncharged, and a keyword this account already had is still handed back uncharged.
- The names shown on the form for "What to return" and "Network" now select the option they label. Both fields offer a choice, and the wording the form displays for the non-default option in each — "Keyword ideas from my keywords" and "Google search and search partners" — was not recognised, so typing what you saw quietly used the default instead: you asked for keyword ideas and received metrics, or asked for partner volumes and received Google-only ones. Both now select what they name, along with several everyday variants.
- Everything else about these two fields is unchanged. A value neither of them recognises still uses the documented default and tells you so on its own uncharged row, and your keywords and market are still looked up exactly as you asked — that is the behaviour described in the note sent on 4 September and it has not been taken back.
- Nothing else moved: no price, event, output column or charge changed, and only delivered results are charged.
- A block of links pasted into the keywords box now comes back with an uncharged row saying so. "Keywords" looks up search volume for words. A whole block of links pasted into a single row used to be sent to the search-volume source as one keyword — a paid lookup for something nobody searches. It is now answered before anything is looked up with a single uncharged row that says how many links it found, and it points at "Keyword list (paste)", which does split a pasted list on lines and commas.
- Your search text is never split apart. Only you know whether "red running shoes" is one search or three, so nothing is guessed — a row holding words rather than links runs exactly as before.
- Nothing else moved: no price, event, output column or charge changed, and only delivered results are charged.
- Rows this actor had already paid for are no longer thrown away when a run's time limit ends. A keyword any earlier run looked up is answered from this actor's own 30-day store of Google's figures, with no new lookup at all. The run's clock was being checked between reading one of those answers and writing it to your dataset, so a run that ran out of time could deliver nothing while the answers were already in hand — nothing delivered, nothing charged, and the money for those lookups already spent. The clock now stops the run from looking anything NEW up, exactly as before, and the answers already in hand are still written and charged. A keyword-ideas expansion behaves the same way: once the expansion has been bought, its suggestions reach you. Your own settings are unchanged — your row cap and your maximum total charge still stop a row, because a stored answer is charged like any other, and a run close enough to the platform's own timeout still stops in time to write its summary row.
- The head term suggested for a phrase with no published volume is now a whole phrase. When Google publishes no volume for your exact wording, the row offers a broader phrase built from your own words by dropping the trailing place. A place written in two words was only half dropped — "physical therapy north county san diego" suggested "physical therapy north county san", and "physical therapy solana beach" suggested "physical therapy solana" — and a phrase that ends in no place at all was cut the same way, so "BJJ injury physical therapy" suggested "BJJ injury physical". The cut now recognises the words that open a place name (san, santa, los, new, fort, saint, lake, port and the rest) and the generic ones that close it (beach, city, county, springs, park and the rest), and takes the whole name off; a phrase whose ending is not a place keeps its own two-word head term instead. Nothing is looked up to do it, and the suggestion is still made only from the words you typed.
- Charges are unchanged. A phrase with no published volume is uncharged, a keyword that was not delivered is uncharged, and only delivered keyword rows are charged.
- A keyword Google publishes no volume for now tells you what to try instead. The row used to say the phrase was "too rare to report or grouped under a similar phrase", which pointed at rarity that is usually not there: Google publishes volume per phrase, and its gaps are per phrase too — on one US/English lookup "dentist near me" comes back at 1,830,000 and "physical therapy san diego" at 1,600, while "physical therapy near me" and "physical therapy carlsbad" come back empty. The row now says Google publishes no search volume for that exact phrase, and names the head term to try — your own phrase without its trailing city or "near me" — plus the other move that works, a larger nearby city.
- The run's status line and its summary row count those phrases apart. They used to sit inside the general list of things that went wrong, beside rate limits and source outages, where a re-run reads like the fix. They now have their own sentence — "N phrases have no published volume — try the head term or a nearby city" — so a run that delivered nothing says why in the first line you read.
- The README says it too. A new "three things about Google's numbers" point spells out that the gaps are per phrase and not per topic, with the measured examples and the head-term advice.
- Charges are unchanged. A phrase with no published volume is not charged, was never charged, and the run still finishes successfully.
- A country this actor cannot look up is now counted against every keyword it stopped. When the country (or language) you set is not one the search-volume source reports on, nothing can be looked up and every keyword you sent gets its own uncharged row saying so. The run's own record of the outcome counted that refusal ONCE, whatever the size of the list — so a run of eight keywords reported one problem and seven keywords named by nothing. The refusal now counts the keywords it stopped. Your rows and the status line are unchanged.
- Keywords a stop never reached are now named in the run's record. When your row cap or budget fills, the run's clock runs out, this actor's per-run source-request limit is reached, or a run is moved to another server, the keywords left over were only listed in the resume cursor. They are now counted in the record as well, so nothing the run planned goes unexplained.
- Keyword IDEAS are now counted as what they are. In "ideas" mode the record compared the many suggestion rows delivered against an ask of one seed keyword, which never added up. The ask for a seed is now the suggestions its expansion actually returned.
- A setting that could not be read and fell back to its default is reported as a note, not as a missing keyword. An unreadable "network" or "search mode" still gets its uncharged row naming the field and the accepted values, and the keyword itself is still delivered — it is simply no longer counted as work the run did not do.
- Charges are unchanged. Nothing is charged for a keyword that was not looked up, and only delivered keyword rows are charged.
- A run that waits now always keeps room to finish and report. When the search-volume source refuses a request on every try, the run can pause and submit it once more — but the check that decided whether a pause was affordable kept back only half a minute for that second submission, while a submission is given two whole minutes. On a run whose time limit was short, the pause was allowed and the submission after it then ran past the end of the run, so the honest uncharged rows it was taken for never landed. The check now keeps back a whole submission, measured from the source's own request limit, so a pause is only taken when the run can finish the submission the pause was for. A run with room to wait waits exactly as it did before.
- Charges are unchanged. Waiting is uncharged, a refused request is uncharged, and only delivered keywords are charged.
- A run started with no input at all now has room to wait out a refusal. Press Start on the untouched form and this actor looks up two example keywords, charged like any run. The search-volume source sometimes refuses every request for a few minutes at a time, and one round of tries could use up most of the sample's own two-and-a-half-minute limit — so the wait-and-try-again this actor gained in the last build could never happen on the most common first run of all: it reported "please re-run" while the refusal was still lifting. The sample's time limit is now seven minutes, sized to hold one full round of tries, one pause, one more submission, and enough time left over to stop cleanly and report. A sample the source answers still finishes in seconds; a re-submitted request costs nothing extra, because the source bills a request it answers and never one it refuses; and nothing is charged unless rows are delivered.
- How much of its remaining time a refused run may spend waiting now follows the size of what you asked for. A run with more requests still to send keeps exactly the limit it had — no more than half the time left — so a wide refusal still leaves the later requests their share of the run. A run with a single request to send may spend what is left on its one pause, because nothing else is waiting for that time.
- The no-input sample now always looks the keywords up. It used to be answered from this account's own history of past runs, so a second Start handed the first one's figures straight back. The sample now ignores that history in both directions: it looks the keywords up every time and does not add its own to the history. A run with your own keywords in it is unchanged — a keyword this account already paid for inside the 30-day window is still handed back to you, not charged.
- A data request the source refuses is now tried again after a short wait, instead of ending as "please re-run" while most of your run's time is still unused. When the search-volume source rate-limits a request or is briefly unavailable, this actor already retried it a few times over about twenty seconds; after that, every keyword in that request came back as an uncharged row with a re-run note — even on a run that still had many minutes left. Now, when the run still holds the time and the keywords waiting on that request are worth the idle minutes, it waits 90–180 seconds and submits the same request once more. A refused request costs nothing at the source, so the wait is the only cost, and it is ours. A keyword answered on the second try is delivered and charged exactly as if the first try had answered.
- What still stops at once: a keyword Google Ads will not accept, a keyword with no data, and a problem with our own account at the source. None of those change in three minutes, so none of them wait.
- A short run never waits. A run at or near its time limit, and the two-keyword sample, still get the single request they always did, and the honest rows ship inside the run's own time.
- The rows and the summary say what happened. A keyword still refused after the wait keeps its uncharged row, now with "We tried again after waiting N minutes in total across 2 passes; it was still refused." before the charge sentence. The run's
_summary row carries patience (requests refused, waits, seconds waited, requests recovered) whenever a request was refused, and the run log says how many requests the wait recovered. Nothing about what is charged changes: only delivered keyword rows are charged, and a wait is never charged.
- A run near its time limit now stops cleanly instead of retrying a data request past it. When a data request is briefly throttled, this actor retries the request a couple of times with a pause between tries; those retries now watch the run's own clock, so a run at its time limit ends with the keywords it already has rather than being cut off part-way through a retry.
- Pick a country, a network or a limit but paste no keywords and you now get the sample, not a note asking for keywords. Pressing Start with nothing at all always looked up the two example keywords. Changing one thing first — your market, "What to return", a row cap — used to cancel it: you received one uncharged row asking for keywords and no data at all. Narrowing a setting made the run worse than touching nothing, which is backwards. Now the sample runs under what you set: your market, your network, your mode, your limits, and everything you did not set takes the sample's own value. A limit larger than the sample's own is capped at it, so a sample can never grow into a bill you did not ask for; a limit smaller than the sample's still binds.
- Asking for keyword ideas with no keywords pasted returns real ideas, and stays a sample: one keyword expanded, at most five ideas, at most ten rows.
- One extra uncharged row on those runs names the settings it used, in the same words the form uses, and says how to run your own list. It is not a miss and nothing is charged for it.
- A field name this actor does not recognise, a keyword field sent as the wrong type, and a country or language nobody can place all still get their own uncharged row. Each is you asking for something the sample would answer wrongly, and each names only the field to fix.
- A "What to return" or "Network" value we did not recognise no longer leaves a run with nothing at all. It already ran on the documented default and said so on its own row; when no keywords were pasted it also cancelled the sample, so the run ended holding one note and no data. The default answer now comes with the sample.
{} is untouched. The bare Start delivers exactly what it delivered before, at the same price, with no extra row, and the owner's own health check still buys no lookup.
Every row of a refused run now tells you the same thing, and names the field.
1.0.36 started saying plainly when a country has no data at the source rather than implying you had mistyped it — but only on the row about that field. The rows for your individual keywords, and the run summary, still said the value "could not be matched", which reads like a spelling problem. All three now carry the same answer, so it does not matter which row you read first.
Each refusal also names its own field now. A network, mode, keywords or keyword list (paste) value we could not read is reported against that field by name, the way country and language already were. Nothing about what you are charged changes — these rows have always been uncharged — but a run that goes wrong is now specific about which field to look at instead of just saying the input could not be read.
27 markets we were wrongly refusing now work, including Hong Kong, Taiwan and Puerto Rico.
The country table shipped with this actor was built by asking the search-volume source for its locations and keeping the ones it labels "Country". That label leaves out territories and special administrative regions — so Hong Kong, Taiwan, Macao, Puerto Rico, Kosovo, Palestine, Gibraltar, Greenland, the Cayman Islands, Bermuda and 17 more were refused, even though the source reports search volume for every one of them. The table is now built from what the source actually offers as a market, and covers all 244.
A country with no data now says so, instead of asking you to check your spelling. Six countries have no search-volume data at the source at any price — Russia, Belarus, Iran, Cuba, North Korea and the Åland Islands. Asking for one used to return the same "use a two-letter code or the English name" advice as a typo, which is no help when what you typed was correct. Now the run names the country, says plainly that no spelling will fix it, and points at the nearest markets that do have data.
Region-wide asks get their own answer too. worldwide, global, EU, Europe, EMEA and the like now explain that search volume is reported one country at a time, rather than reading as a typo.
And the Google Ads location code works as a country. If you already have 2840 or 2826 to hand from the ads API, send it.
All of this is uncharged, one row per keyword, and the run still succeeds.
A country or language we do not recognise no longer costs you the whole run.
USA, UK, united-states, en-US, en_us and eng were all refused before this build — and one refused value emptied the entire request, so a run with eight good keywords looked up none of them and returned a single guidance row. That is fixed twice over.
Spellings we now understand. Countries take a two-letter code, a three-letter code, or the English name, with the everyday variants: US, USA, America, GB, UK, Britain, United Kingdom, AE, UAE, SA, KSA, KR, Korea. Languages take a code, a BCP-47 tag, or the English name: en, eng, en-US, en_GB, pt-BR, English. Case, spaces, hyphens, underscores and accents no longer matter. network and mode are just as forgiving — google_search, partners, metrics, keyword-ideas all land where you would expect.
And when a value genuinely cannot be placed, the run refuses that one field instead of your whole request. Every keyword you sent comes back as its own uncharged row naming the field that stopped it and the spellings that work, so you can see exactly what to change. Nothing is charged, and the run still succeeds.
network and mode work the other way round now. They only widen or narrow an answer about the keywords and market you did name, so a value we do not recognise there runs on the documented default, says so on an uncharged row naming the field — and your keywords are still looked up and delivered. Previously a single mistyped mode bought nothing at all.
Country and language are dropdowns on the console, listing every market Google Ads reports on, so a value that cannot work is not one you can pick. The API still accepts any string, which is what the spellings above are for.
You are never charged twice for the same keyword.
Ask for a keyword you already got from us inside the last 30 days and the same Google Ads figures are handed straight back, with nothing charged for them. No lookup is bought and no keyword-result event is charged — the check happens before either — so re-running a list, adding ten keywords to a list of a hundred, or resuming a stopped run costs you only what is genuinely new.
Those rows say so: repeat: true, charged: false, firstSeenAt and firstSeenRunId, plus a note naming the day and the run. The run's status line says how many were handed back.
Past 30 days Google has refreshed its figures, so the same keyword is new data and a new charge — the same window the shared 30-day cache uses, and for the same reason. Note the difference between the two: that cache is shared across everyone who uses this actor and only saves the lookup, so a keyword someone else looked up is still your first one.
The memory is yours: it lives in your own account, in a key-value store called kw-volume-account on your Storage tab — delete it to start over and be charged again. On a run where it cannot be read, the run still delivers and charges exactly as it did before, and the status line says the check was unavailable so you know a repeat could have been billed.
Also: the run status line is shorter, so the counts, the named cap and the support ask survive on the run page.
- A keyword Google Ads rejects is now reported as rejected, not as a temporary outage. The row for such a keyword always said to edit it and start a new run, but the run summary above it counted the keyword as "temporarily unavailable (re-run)" — so following the summary bought another lookup for a keyword that will be rejected every time. The summary and the row now agree.
- A run that stops before it starts now reports its outcome too — a run refused for its memory setting, and a run stopped because this build was missing a credential of ours, now reach us the same way every other run does: counts and reason codes only, never your input or your rows.
- Every run now reports its own outcome to us — counts and reason codes only, never your input or your rows — so a run that goes wrong reaches us even when nobody shares it.
- A stray double full stop in the run summary is gone.
- Run status lines fit the run page again; sample-run wording shortened.
A run that resumes after a platform restart now recognises every row it already delivered, so nothing is delivered or charged twice.
Apify occasionally moves a running actor to another server. When that happens, the run re-reads its own dataset to remember what it already delivered. Until now it trusted the dataset's row count, which can lag for a moment after a restart; a lagging count could make the run start over and charge again for rows you already had, stop before the end, or stop the run outright as a precaution. The run now checks for real rows instead of trusting the count, and reads to the end whatever the count says. Rows, prices, charges and the status line on a normal run are exactly as before.
- A run that filled the row cap you set is no longer treated as a run that went wrong. The Issues-tab line now appears only when something actually did.
- A run that ends with a problem now says where to reach us. A miss, an input error, an early stop or a failed run closes by pointing at the Issues tab and naming the reply time; a fully delivered run is left alone.
- One support promise across this page — issues are answered in a couple of hours, always within a day.
Fresh lookups are back on. The daily lookup budget introduced in 1.0.24 is gone: nothing about a run is refused automatically. Fresh lookups can still be paused on our side for maintenance; then cached keywords are delivered and charged as usual and one uncharged row names the rest. Prices are unchanged.
Fresh lookups are paused until 16 September 2026. A run still delivers every keyword already in the 30-day cache, charged as usual, and ships one uncharged row naming the remaining keywords and when to re-run (resumeCursor lists them). Nothing is charged for a keyword that was not looked up. Prices are unchanged.
Fresh lookups now sit behind a daily budget on our side. Every new search-volume lookup this actor makes has a cost, and from this build the actor checks its own daily budget before making one. When the day's budget is used up — or while we have paused fresh lookups for maintenance — a run still delivers every keyword already in the 30-day cache, charged as usual, and ships one uncharged row saying that the remaining keywords were not looked up and when to re-run; the summary row's resumeCursor lists them. Prices are unchanged, and nothing is charged for a keyword that was not looked up.
An unrecognised value in the Network or "What to return" dropdown now stops the run at one uncharged guidance row naming the value and the accepted o