Google Trends Bulk Compare – Many Keywords, One Scale
Pricing
$3.00 / 1,000 keyword timeline with data
Google Trends Bulk Compare – Many Keywords, One Scale
Put many Google Trends keywords on one shared 0–100 scale (tested up to 30 per run) via anchored groups. Custom dates, countries & subregions, categories, optional rising queries. Flat CSV. $0.003 per keyword with data; failed keywords free, no start fee. No proxy or Google account needed.
Pricing
$3.00 / 1,000 keyword timeline with data
Rating
0.0
(0)
Developer
星賢 陳
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
11 days ago
Last modified
Categories
Security
Limited permissionsShare
Google Trends Bulk Compare
Put dozens of Google Trends keywords on one shared 0–100 scale. US$0.003 per keyword that returns data; failed or empty keywords are free, and there is no start fee.
Try it: click Start with the prefilled input, which compares 6 keywords (coffee, tea, matcha, bubble tea, espresso, latte; Taiwan, past 12 months) on one shared 0–100 scale for at most US$0.018 (6 × US$0.003).
What does Google Trends Bulk Compare do?
This Actor fetches Google Trends interest over time for a list of keywords and returns flat rows, one row per keyword per date, as a dataset and as OUTPUT.csv.
- Put many keywords on one shared scale (
compareAcrossGroups). A Google Trends Explore chart compares only a limited number of terms, and every chart has its own 0–100 scale, so values from different charts can't be compared directly. This Actor splits your list into groups of an anchor term plus up to 4 other keywords. Each group is then rescaled using the anchor (factor = sum(anchor in the reference group) / sum(anchor in this group)), and the overall maximum is set to 100. Rows are markedscale: "anchored-global"together with theirgroup. The factors, warnings and group membership are saved asRESCALE_INFO. - Or one scale per keyword (the default). Each keyword gets its own 0–100 scale, marked
scale: "per-keyword". - Custom date ranges (
customTimeRange), categories (category), and countries or subregions (geo, e.g.US,US-CA,TW-TPE; leave it empty for worldwide). - Related queries (optional): top and rising lists, with an
isBreakoutflag on each query. - Interest by region (optional).
- Change since last run (
trackChanges): when you run the same geo, time range, category and anchor again, theKEYWORD_SUMMARYrecord addspreviousLatestValue,deltaSinceLastRunandnewRisingSinceLastRun. - No browser, no proxy, no login. It uses plain HTTP JSON calls that identify the Actor in the User-Agent (
RondelBot/1.0), with a 30 s timeout and, by default, about 8 s between keywords (or groups in compare mode). Only HTTP 408, 429, 5xx and network errors are retried, with exponential backoff plus jitter. The Actor honoursRetry-Afterand fetches a fresh cookie after the first 429. A 400 fails immediately with a message that points to the inputs Google rejected (usually an invalidgeo, time range or category). That keyword is free. - Each keyword succeeds or fails on its own. Every row has a
status(ok,partial,errororskipped-charge-limit). Values come only from Google's responses; missing data is reported as an error, not filled in. - Budget-aware. Once your run's maximum charge is reached, the remaining keywords are marked
skipped-charge-limit. They are not processed and not charged.
How to put many Google Trends keywords on one scale
- Put all your keywords in
keywords. - Set
compareAcrossGroups: trueand choose a steady, fairly popularanchorTerm(it is included in every group of 5). If the anchor is not one of your keywords, its data is still returned as a free scale reference. - Run the Actor. Every keyword comes back on one shared 0–100 scale (
scale: "anchored-global"), and the rescale factors and the low-anchor warning (if it fired) are saved inRESCALE_INFO.
Example input (the prefilled input)
{"keywords": ["coffee", "tea", "matcha", "bubble tea", "espresso", "latte"],"geo": "TW","timeRange": "today 12-m","compareAcrossGroups": true,"anchorTerm": "coffee"}
We ran this exact input on the Apify platform (internal test, 2026-10-17): the Actor made 2 groups (coffee + 4 keywords, and coffee + latte) and returned 318 rows on one anchored-global scale. 6 keywords with data = 6 charges = US$0.018.
Google Trends data you can export
| Field | Description |
|---|---|
keyword | The search term |
date | Date of the data point (null for a failed keyword) |
value | Interest on the 0–100 scale (decimals after rescaling) |
isPartial | true if Google marks the period as incomplete |
scale | per-keyword or anchored-global |
group | Group index in compare mode |
geo, timeRange, category | The request settings |
status | ok, partial, error or skipped-charge-limit |
error | Error message for failed keywords |
isAnchorOnly | true only for rows of an anchor term that is not one of your keywords (compare mode); returned as a scale reference (or as an error row if the anchor returned no usable data) and never charged |
charged | true for rows of a keyword you were charged for; false for anchor-only, error and skipped rows |
How much does it cost to scrape Google Trends?
Pay per event: US$0.003 per keyword that returns data.
- Charged for keywords with
statusokorpartial.partialmeans the timeline came back but an optional section failed. - Keywords that end in
errororskipped-charge-limitare free. - In compare mode, an
anchorTermthat is not one of yourkeywordsis free: its data is returned as a scale reference (isAnchorOnly: true,charged: false) and it does not count as an ok or failed keyword. If the anchor is also inkeywords, it is charged once, like your other keywords. - There is no start fee.
- Platform usage is included in the price, so you don't pay a separate compute or proxy charge for this Actor.
This price is provisional until we measure the Actor's cost on the Apify platform, and it may change. Apify's rules on price-change notice apply.
Example: 30 keywords that all return data cost 30 × US$0.003 = US$0.09.
Use cases for SEO and content agencies
- Client keyword watchlists on one scale: compare 20–30 client keywords on one chart, instead of several separate charts that each have their own scale.
- Weekly monitoring: schedule runs in Apify and turn on
trackChangesto see how much each keyword moved since the last run, plus rising queries that are new since then. - Content ideas: pull rising and breakout related queries around your seed keywords.
- Local SEO: use subregions (e.g.
US-CA) and interest by region. - Seasonal planning: choose a custom date range and a category (e.g. 71 = Food & Drink).
- Reporting: the flat CSV (one row per keyword per date) goes straight into Sheets, Excel, Looker Studio or a BI tool.
Input
| Field | Type | Default | Notes |
|---|---|---|---|
keywords | array of strings | (required) | No hard limit; tested up to 30 per run (internal test, 2026-10-17). Blank entries are dropped. Duplicates are dropped before anything is fetched or charged; matching ignores letter case and extra spaces, so Chess and chess count as one keyword (the first spelling is kept). If no keyword is left, the run stops with a clear message and nothing is charged. |
geo | string | "" (worldwide) | Country code (e.g. US) or subregion (e.g. US-CA, TW-TPE). |
timeRange | string | "today 12-m" | Google Trends time string, e.g. "now 7-d", "today 5-y". |
customTimeRange | string | — | "YYYY-MM-DD YYYY-MM-DD". Overrides timeRange when set. |
category | integer | 0 | Google Trends category ID (0 = all; e.g. 71 = Food & Drink). |
compareAcrossGroups | boolean | false | Puts all keywords on one shared 0–100 scale, using anchored groups. |
anchorTerm | string | first keyword | Included in every group. Pick a steady, fairly popular term: anchors that average below 5 amplify rounding error. If it is not one of your keywords, it is returned as a free scale reference (isAnchorOnly: true). |
includeRelatedQueries | boolean | false | Top + rising queries, with isBreakout. Adds 1 extra request per keyword. Results go to the RELATED_QUERIES record in this run's key-value store. |
includeInterestByRegion | boolean | false | Adds 1 extra request per keyword. Results go to the INTEREST_BY_REGION record in this run's key-value store. |
trackChanges | boolean | false | Reports change since the last run with the same geo, time range, category and anchor. |
stateStoreName | string | "trends-state" | Named key-value store used by trackChanges. |
delayBetweenKeywordsMs | integer | 8000 (min 2000) | Pause between keywords, or between groups in compare mode. |
delayBetweenRequestsMs | integer | 2500 (min 0) | Pause after every successful request. |
maxRetries | integer | 4 (0–8) | Applies only to 408/429/5xx/network errors. |
baseDelayMs | integer | 5000 | Base delay for backoff; it doubles on each retry, plus jitter. |
maxDelayMs | integer | 120000 | Upper limit on each backoff or Retry-After wait. |
hl | string | "en-US" | Language for Google Trends labels. |
tz | integer | -480 | Timezone offset in minutes (Google's convention: -480 = UTC+8). |
datasetLayout | string | "flat" | flat = one dataset row per keyword per date. keyword = one dataset item per keyword with a nested timeline and the isAnchorOnly and charged flags (about 50x fewer items, cheaper to store). OUTPUT.csv is flat in both layouts. |
Example 1 – the prefilled input: 6 keywords on one scale (Taiwan, past 12 months)
{"keywords": ["coffee", "tea", "matcha", "bubble tea", "espresso", "latte"],"geo": "TW","compareAcrossGroups": true,"anchorTerm": "coffee"}
Example 2 – minimal (worldwide, past 12 months, one scale per keyword)
{ "keywords": ["content marketing", "seo audit", "link building"] }
Example 3 – US, 12 keywords compared on one scale, with weekly change tracking
{"keywords": ["seo", "content marketing", "link building", "keyword research", "seo audit","local seo", "technical seo", "backlinks", "google ads", "email marketing","social media marketing", "copywriting"],"geo": "US","compareAcrossGroups": true,"anchorTerm": "seo","includeRelatedQueries": true,"trackChanges": true}
Example 4 – subregion, category and custom date range (the input from our own test)
{"keywords": ["coffee"],"geo": "US-CA","category": 71,"customTimeRange": "2026-01-01 2026-06-30"}
Output
Default dataset / OUTPUT.csv. Flat rows, one per keyword per date. The rows below are real, taken from our internal anchor test on the Apify platform (2026-10-17: keywords tea and matcha, anchor coffee not in the keywords, TW, past 12 months) and cut down to 3 rows. coffee is the free scale reference:
keyword,date,value,isPartial,scale,group,geo,timeRange,category,status,error,isAnchorOnly,chargedcoffee,2025-10-12,39,false,anchored-global,0,TW,today 12-m,0,ok,,true,falsetea,2025-10-12,19,false,anchored-global,0,TW,today 12-m,0,ok,,false,truematcha,2025-10-12,2,false,anchored-global,0,TW,today 12-m,0,ok,,false,true
Rows from an earlier subregion/category test (cut down to 2 of 180 daily rows; that version did not yet write the last two columns, so they are shown here as the current version fills them for a charged keyword):
coffee,2026-01-01,86,false,per-keyword,,US-CA,2026-01-01 2026-06-30,71,ok,,false,truecoffee,2026-01-02,77,false,per-keyword,,US-CA,2026-01-01 2026-06-30,71,ok,,false,true
If a keyword fails, it gets a single row with date: null, value: null, status: "error" and an error message.
KEYWORD_SUMMARY record (default key-value store of the run, flat layout). One entry per keyword, tagged with runId. The row below comes from our local mock test (test-mock-suite.log, second of 2 runs). It does not contain Google data and is shown only to illustrate the structure. Entries from the current version also include isAnchorOnly and charged, like the dataset rows:
{"keyword": "alpha", "status": "ok", "group": null, "scale": "per-keyword", "error": null,"latestValue": 100, "latestDate": "2025-10-23","rising": ["alpha rising 2", "alpha new"], "breakout": ["alpha new"],"previousRunAt": "2026-10-16T19:23:23.552Z", "previousLatestValue": 75,"deltaSinceLastRun": 25, "newRisingSinceLastRun": ["alpha rising 2"]}
RELATED_QUERIES record (default key-value store of the run). One entry per query, tagged with runId. This is an example of the structure, not real output from the current version; the query values come from an earlier test run:
{"keyword": "bubble tea", "list": "rising", "query": "best bubble tea near me", "value": 109300,"formattedValue": "Breakout", "isBreakout": true, "geo": "TW", "timeRange": "today 12-m", "category": 0}
The key-value store also holds RUN_SUMMARY (counts, requests, 429s, charged events, and anchorOnlyNotCharged) and, in compare mode, RESCALE_INFO. anchorOnlyNotCharged is 1 when one free anchor-only row was returned (an anchor that is not one of your keywords), otherwise 0. It does not mean the anchor had data: it is also 1 when the anchor failed and came back as an error row.
When an anchor that is not one of your keywords is involved, the run's status message ends with one of these:
anchor "<term>" returned as scale reference only (not charged)when the anchor returned data;anchor "<term>" returned no data (not charged)when it did not, for example after HTTP 429 errors that did not recover or when the anchor is all zeros.
If Google rejected every request as invalid (HTTP 400), the status message is instead: No data: Google rejected every request as invalid (HTTP 400). Check geo (…), the time range (…) and category (…). Nothing was charged., with your geo, time range and category filled in.
Accuracy and tested limits (internal tests, 2026-10-17 Taipei time)
These numbers come from our own small internal tests. They are not a guarantee.
- Matched Google's own CSV export (internal test, 2026-10-17): we fetched the same request from the endpoint that Google Trends' download-CSV button uses. A 5-term request (TW, past 12 months) had 265/265 values identical (100%), max diff 0. A 3-term group had 159/159 identical, max diff 0. In total, 424/424 identical.
- One-scale accuracy: anchored rescaling (anchor = ramen, two groups) was compared with Google's direct 5-term request for coffee, bubble tea, hot pot, night market and ramen (TW, past 12 months). Over 265 points: MAE 0.110, max absolute error 0.65 on the 0–100 scale. The error comes from Google rounding values to integers. In this test the anchor's mean was only 4.04, so the low-anchor warning fired.
- Same data with the current User-Agent (internal test, 2026-10-17): the same 20 keywords (US, past 12 months, per keyword) were run on the Apify platform once with the current
RondelBot/1.0User-Agent and once with standard browser-like headers. Both runs: 20/20 ok, 1,060 rows, 0 HTTP 429, and all 1,060/1,060 values identical. - 30-keyword run on the Apify platform (internal test, 2026-10-17, version 0.3.16, with the current User-Agent): 30 keywords, worldwide, past 12 months, per keyword, default 512 MB, no proxy: 30/30 ok, 1,590 rows, 64 requests, about 7 minutes (411 s). There were 2 × HTTP 429, both recovered after backoff.
- Compare mode on the Apify platform (internal test, 2026-10-17, version 0.3.17): 6 keywords (coffee, tea, matcha, bubble tea, espresso, latte), anchor
coffee, Taiwan, past 12 months. The Actor split them automatically into 2 groups (coffee + 4 keywords, and coffee + latte) and returned 318 rows, all on oneanchored-globalscale, with coffee at the peak of 100. 6 charges = 6 keywords with data. There were 3 × HTTP 429, all recovered after backoff. - Anchor not in your keywords (internal tests on the Apify platform, 2026-10-17, versions 0.3.18 to 0.3.20): keywords
teaandmatchawith anchorcoffee(TW, past 12 months, compare mode): 2 charges for the 2 keywords, and coffee's 53 rows came back withisAnchorOnly: trueandcharged: false. Withcoffeealso in the keywords: 3 charges, coffee charged once. The same free-anchor case withdatasetLayout: "keyword"(version 0.3.20): 3 items, coffee withisAnchorOnly: trueandcharged: false, 2 charges. - Anchor not in your keywords that fails (local mock only, not tested against Google or on the Apify platform; version 0.3.20): with the anchor's requests failing with HTTP 429, failing with HTTP 400, or returning all zeros, every keyword in the anchor's group came back as a free
errorrow, the anchor came back as anerrorrow withisAnchorOnly: trueandcharged: false(not counted as ok or failed), the run finished normally without crashing, and nothing was charged. - 429 handling (local mock, not Google): the Actor honoured
Retry-After, gave up cleanly after retries ran out, and did not retry a 400. - Not measured: more than 30 keywords per run with the current User-Agent, compare mode with more than 6 keywords on the Apify platform, a failing anchor against real Google responses, sustained or scheduled use, and Google's heavy throttling.
Google Trends API with Python, JavaScript and curl
Replace <YOUR_APIFY_TOKEN> with your token from Apify Console → Settings → API & Integrations.
How long a run takes: in per-keyword mode (the default), about 14 s per keyword with the default delays. In our internal runs on the Apify platform (2026-10-17), runs of 20–30 keywords took 12–14 s per keyword, and 30 keywords took about 7 minutes (411 s). Compare mode is faster, because each group of up to 5 keywords is fetched together. Apify's synchronous endpoint (run-sync-get-dataset-items) stops waiting after 300 s and returns HTTP 408 (the run itself keeps going), so per-keyword lists of more than about 20 keywords can time out there. Use the API client's .call(), which waits for the run to finish, or start the run asynchronously and fetch the results when it is done.
Python (pip install "apify-client>=3"; .call() waits until the run finishes):
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("raspberry_rondel/actor-google-trends").call(run_input={"keywords": ["coffee", "tea", "matcha", "bubble tea", "espresso", "latte"],"geo": "TW","compareAcrossGroups": True,"anchorTerm": "coffee",})for row in client.dataset(run.default_dataset_id).iterate_items():print(row["keyword"], row["date"], row["value"], row["status"])
JavaScript / Node.js (npm install apify-client; .call() waits until the run finishes):
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('raspberry_rondel/actor-google-trends').call({keywords: ['coffee', 'tea', 'matcha', 'bubble tea', 'espresso', 'latte'],geo: 'TW',compareAcrossGroups: true,anchorTerm: 'coffee',});const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(items);
curl, asynchronous (works for any list size):
# 1. Start the run. The response returns at once; note "id" (the run ID).curl -X POST "https://api.apify.com/v2/acts/raspberry_rondel~actor-google-trends/runs" \-H "Authorization: Bearer <YOUR_APIFY_TOKEN>" \-H "Content-Type: application/json" \-d '{"keywords": ["coffee", "tea", "matcha", "bubble tea", "espresso", "latte"], "geo": "TW", "compareAcrossGroups": true, "anchorTerm": "coffee"}'# 2. Check the run. Each call waits up to 60 s; repeat until "status" is "SUCCEEDED".curl "https://api.apify.com/v2/actor-runs/<RUN_ID>?waitForFinish=60" \-H "Authorization: Bearer <YOUR_APIFY_TOKEN>"# 3. Download the results (format=json or format=csv).curl "https://api.apify.com/v2/actor-runs/<RUN_ID>/dataset/items?format=csv" \-H "Authorization: Bearer <YOUR_APIFY_TOKEN>"
curl, synchronous (small lists only: runs longer than 300 s return HTTP 408):
curl -X POST "https://api.apify.com/v2/acts/raspberry_rondel~actor-google-trends/run-sync-get-dataset-items" \-H "Authorization: Bearer <YOUR_APIFY_TOKEN>" \-H "Content-Type: application/json" \-d '{"keywords": ["coffee", "tea", "matcha"], "geo": "TW"}'
FAQ
Is there a Google Trends API? This Actor gives you API access to Google Trends interest-over-time data through the Apify API (see the examples above). It uses Google Trends' web endpoints, not an official Google API.
Why might values differ from what I see in the browser? Google Trends computes values from a sample of searches and rounds them to integers, and it refreshes recent data over time (the newest point is often marked partial). A request made at a different time can therefore return slightly different numbers, in the browser or here. In an internal test (2026-10-17), values fetched at the same time matched Google's CSV export exactly (424/424).
How do I put more keywords on one scale than one Google Trends chart allows? Set compareAcrossGroups: true and choose a steady, popular anchorTerm. Keywords are split into groups of the anchor plus up to 4 others, and each group is rescaled through the anchor onto one 0–100 scale. In an internal test (2026-10-17) against Google's direct 5-term compare, the error was MAE 0.110 and max 0.65 over 265 points. If the anchor averages below 5, the run logs a low-anchor warning, because rounding error grows; factors and warnings are saved in RESCALE_INFO.
Does it match what I see on Google Trends? In an internal test (2026-10-17), values matched Google's CSV export exactly (424/424: 265/265 and 159/159). Values rescaled onto one scale differed from Google's direct 5-term compare by MAE 0.110 (max 0.65) over 265 points.
How many keywords per run? The code has no fixed cap. Our largest internal test with the current User-Agent (2026-10-17) was 30 keywords in per-keyword mode, which took about 7 minutes (about 14 s per keyword). Compare mode sends fewer requests, because each group of up to 5 keywords is fetched together. With the default 1-hour timeout, plan for at most about 200 keywords per run in per-keyword mode.
Is it search volume? No. Google Trends gives relative interest on a 0–100 scale.
What if Google rate-limits me? The Actor retries 408/429/5xx errors with backoff and honours Retry-After. If a keyword still fails, it gets an error row, is not charged, and the run continues.
Am I charged for the anchor term? Only if it is one of your keywords, and then once. An anchorTerm that is not in keywords is returned as a scale reference so you can see what the other keywords were scaled against. Its rows have isAnchorOnly: true and charged: false, it is not counted as an ok or failed keyword, and it costs nothing. Because the anchor is part of every group, a failed anchor makes the keywords in its group fail too; those keywords get free error rows, and the anchor comes back as a free error row.
Can I cap my spend? Yes. Set a maximum total charge for the run; the remaining keywords are then skipped and not charged.
Do I need a proxy or a Google account? No.
Does it collect personal data? No. It only fetches aggregated Google Trends data.
Is it legal to scrape Google Trends? This Actor reads the same JSON endpoints that the Google Trends website uses (trends.google.com/trends/api/...). They are not an official, documented Google API, and Google can change, limit or block them without notice. Google's Terms of Service prohibit "using automated means to access content from any of our services in violation of the machine-readable instructions on our web pages (for example, robots.txt files…)". The robots.txt on trends.google.com does not disallow the paths this Actor calls, but the robots.txt on www.google.com disallows /trends/api. You are responsible for making sure your use fits Google's terms and the laws that apply to you. If you have access to the official Google Trends API (alpha), prefer it. The data is aggregated and contains no personal data. This is general information, not legal advice.
How should I credit the data? Google asks you to attribute reused Trends data, for example: "Data source: Google Trends (https://www.google.com/trends)."
Limitations
- Large lists: the code has no keyword limit, but our largest tested run with the current User-Agent was 30 keywords. Requests run one at a time with polite delays, so very large lists take longer and are more exposed to Google rate limits. For big lists, consider splitting them across runs.
- The one-scale values are estimates. Rescaling is accurate only to within rounding error (see the measured numbers above). The error grows when the anchor is not very popular (mean below 5) or when a group needs a large factor. If the anchor is all zeros in a group, or its request for that group fails, that group's keywords get
errorand are not charged. If a group's dates don't line up with the reference group, that group also getserror. - Compare mode can fetch before skipping. A group is fetched before its members are charged, so when you hit your spending limit, part of a group can be fetched and then skipped. This uses requests but costs you nothing.
- Lightly tested: change tracking, related queries and interest by region were checked in one pair of small real runs (2 keywords, US, per-keyword mode; internal test, 2026-10-17). Compare mode itself has been tested on the platform (see above), but these three features have not been tested in real runs in compare mode or on large lists.
- Google Trends values are relative indexes (0–100), not search volumes. Google can return empty data for some terms.
- Related topics are not supported.
- The Actor depends on unofficial Google Trends web endpoints, which can change without notice.
Disclaimer
This Actor is an independent tool. It is not affiliated with, endorsed by, or sponsored by Google LLC. "Google" and "Google Trends" are trademarks of Google LLC. Please use the data in line with the applicable terms. If you publish charts or numbers from this data, credit "Data source: Google Trends (https://www.google.com/trends)".