# Google Trends Bulk Compare – Many Keywords, One Scale (`raspberry_rondel/actor-google-trends`) Actor

Compare 5+ Google Trends keywords on one shared 0–100 scale (Google's UI caps at 5). Custom dates, countries & subregions, categories, optional rising queries. Flat CSV output. $0.003 per keyword with data; failed keywords are free, no start fee. No proxy or Google account needed.

- **URL**: https://apify.com/raspberry_rondel/actor-google-trends.md
- **Developed by:** [星賢 陳](https://apify.com/raspberry_rondel) (community)
- **Categories:** SEO tools, Developer tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 keyword timeline with data

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Trends Bulk Compare

**Compare more than 5 Google Trends keywords on one scale.** US$0.003 per keyword that returns data; failed or empty keywords are free, and there is no start fee.

**Built after Google's Sept 2 redesign of Google Trends** (this Actor was built in October 2026). In our own test on 2026-10-17 (Taipei time), **424 of 424 values were identical to Google Trends' own CSV export** (265 values from a 5-term request plus 159 from a 3-term group, max difference 0), fetched from the same widgetdata endpoint the Trends UI's download button uses. You pay only for keywords that return data: **failed or empty keywords are free, and there is no start fee.**

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

- **Compare more than 5 keywords on one scale** (`compareAcrossGroups`). Google's own compare tool accepts only 5 terms. 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 marked `scale: "anchored-global"` together with their `group`. The factors, warnings and group membership are saved as `RESCALE_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 `isBreakout` flag on each query.
- **Interest by region** (optional).
- **Change since last run** (`trackChanges`): when you run the same geo, time range, category and anchor again, the `keyword-summary` dataset adds `previousLatestValue`, `deltaSinceLastRun` and `newRisingSinceLastRun`.
- **No browser, no proxy, no login.** It uses plain HTTP JSON calls with a 30 s timeout. Only HTTP 408, 429, 5xx and network errors are retried, with exponential backoff plus jitter. The Actor honours `Retry-After` and fetches a fresh cookie after the first 429. A 400 fails immediately.
- **Each keyword succeeds or fails on its own.** Every row has a `status` (`ok`, `partial`, `error` or `skipped-charge-limit`). The Actor never invents values.
- **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 compare more than 5 keywords in Google Trends

1. Put all your keywords in `keywords`.
2. Set `compareAcrossGroups: true` and choose a steady, fairly popular `anchorTerm` (it is included in every group of 5).
3. Run the Actor. Every keyword comes back on one shared 0–100 scale (`scale: "anchored-global"`), and the rescale factors and any low-anchor warning are saved in `RESCALE_INFO`.

#### Example input

```json
{
  "keywords": ["chatgpt", "gemini", "claude ai"],
  "geo": "US",
  "timeRange": "today 12-m"
}
```

Add `"compareAcrossGroups": true, "anchorTerm": "chatgpt"` to put more than 5 keywords on one shared scale.

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

### How much does it cost to scrape Google Trends?

**Pay per event: US$0.003 per keyword that returns data.**

- Charged for keywords with `status` `ok` or `partial`. `partial` means the timeline came back but an optional section failed.
- Keywords that end in `error` or `skipped-charge-limit` are free.
- 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 5-term charts that you can't compare.
- **Weekly monitoring:** schedule runs in Apify and turn on `trackChanges` to see how much each keyword moved since the last run, plus any rising queries that are new.
- **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. Blank entries and duplicates are dropped. |
| `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. |
| `includeRelatedQueries` | boolean | `false` | Top + rising queries, with `isBreakout`. Adds 1 extra request per keyword. Results go to the `related-queries` dataset. |
| `includeInterestByRegion` | boolean | `false` | Adds 1 extra request per keyword. Results go to the `interest-by-region` dataset. |
| `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 any 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). |

**Example 1 – minimal (worldwide, past 12 months, one scale per keyword)**

```json
{ "keywords": ["content marketing", "seo audit", "link building"] }
```

**Example 2 – US, 12 keywords compared on one scale, with weekly change tracking**

```json
{
  "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 3 – subregion, category and custom date range (the input from our own test)**

```json
{
  "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 25-keyword local test (TW, past 12 months, anchor `coffee`, `batch25-output.csv`) and cut down to 3 rows:

```csv
keyword,date,value,isPartial,scale,group,geo,timeRange,category,status,error
coffee,2025-10-12,4.5,false,anchored-global,0,TW,today 12-m,0,ok,
ramen,2025-10-12,0.23,false,anchored-global,0,TW,today 12-m,0,ok,
youtube,2025-10-12,81.25,false,anchored-global,4,TW,today 12-m,0,ok,
```

Real rows from the subregion/category test (`sample-subregion-category.csv`, cut down to 2 of 180 daily rows):

```csv
coffee,2026-01-01,86,false,per-keyword,,US-CA,2026-01-01 2026-06-30,71,ok,
coffee,2026-01-02,77,false,per-keyword,,US-CA,2026-01-01 2026-06-30,71,ok,
```

If a keyword fails, it gets a single row with `date: null`, `value: null`, `status: "error"` and an `error` message.

**`keyword-summary` dataset.** One row per keyword. 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:

```json
{"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` dataset.** One row per query. This is an **example of the structure**, not real output from the current version; the query values come from an earlier test run:

```json
{"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, in compare mode, `RESCALE_INFO`.

### Accuracy and tested limits (measured in our own tests, 2026-10-17 Taipei time)

These numbers come from our own small tests. They are not a guarantee.

- **Matches Google's own CSV export:** 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.
- **Batches:**
  - Local: 25 keywords in anchored groups, **25/25 ok, 72.1 s**, 13 requests, 0 non-200 responses.
  - Apify platform (256 MB, no proxy): 8 keywords, per keyword, **8/8 ok, 106.7 s** (platform 109.2 s), with **1 × 429, retried and recovered**.
  - Apify platform: 30 keywords in anchored groups, **30/30 ok, 105.7 s** (platform 107.4 s), 17 requests, 0 non-200.
  - Datacenter IPs on Apify were not blocked: there was 1 transient 429 in 36 platform requests.
- **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, 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.

**curl** (runs the Actor and returns the dataset items when it finishes):

```bash
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": ["chatgpt", "gemini", "claude ai"], "geo": "US"}'
```

**Python** (`pip install "apify-client>=3"`):

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("raspberry_rondel/actor-google-trends").call(
    run_input={"keywords": ["chatgpt", "gemini", "claude ai"], "geo": "US"}
)
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`):

```javascript
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: ['chatgpt', 'gemini', 'claude ai'],
    geo: 'US',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

### 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 our test, values fetched at the same time matched Google's CSV export exactly (424/424).

**How is Google's 5-term limit handled?** 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 our test 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 our tests, 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. We have tested up to 30 per run.

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

**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?** The Actor only fetches aggregated, public Google Trends data and no personal data. It is not legal advice: please check that your use fits the terms that apply to you.

### Limitations

- **Large lists:** the code has no keyword limit, but our largest tested run 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, that group's keywords get `error` and are not charged. If a group's dates don't line up with the reference group, that group also gets `error`.
- **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.
- **Mock-tested only:** change tracking has been tested only against a local mock. Related queries and interest by region have not yet been tested in real runs of the current version, including compare mode.
- 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.

# Actor input Schema

## `keywords` (type: `array`):

Search terms. No hard limit; tested up to 30 per run. Each keyword succeeds or fails on its own. Blank entries and duplicates are dropped.

## `geo` (type: `string`):

ISO country code (e.g. US) or subregion (e.g. US-CA, TW-TPE). Empty = worldwide.

## `timeRange` (type: `string`):

Google Trends time string, e.g. 'today 12-m', 'now 7-d', 'today 5-y'.

## `customTimeRange` (type: `string`):

'YYYY-MM-DD YYYY-MM-DD'. Overrides timeRange when set.

## `category` (type: `integer`):

Google Trends category id (0 = all, e.g. 71 = Food & Drink).

## `compareAcrossGroups` (type: `boolean`):

Splits keywords into groups of anchor + 4, then rescales every group onto one shared scale using the anchor term.

## `anchorTerm` (type: `string`):

Term included in every group (default: first keyword). Choose a steady, fairly popular term: anchors that average below 5 amplify rounding error.

## `includeRelatedQueries` (type: `boolean`):

1 extra request per keyword. Written to the 'related-queries' dataset.

## `includeInterestByRegion` (type: `boolean`):

1 extra request per keyword. Written to the 'interest-by-region' dataset.

## `trackChanges` (type: `boolean`):

Saves the latest values and rising queries in a named key-value store, then reports deltas and new rising queries on the next run with the same geo/time/category.

## `stateStoreName` (type: `string`):

Named key-value store used by trackChanges.

## `delayBetweenKeywordsMs` (type: `integer`):

Polite pause between keywords (or groups in compare mode).

## `delayBetweenRequestsMs` (type: `integer`):

Pause after every successful request.

## `maxRetries` (type: `integer`):

Applies only to 408/429/5xx/network errors.

## `baseDelayMs` (type: `integer`):

Base delay for exponential backoff (doubles each retry, plus jitter).

## `maxDelayMs` (type: `integer`):

Upper limit on any backoff or Retry-After wait.

## `hl` (type: `string`):

Language for Google Trends labels.

## `tz` (type: `integer`):

Google's convention: -480 = UTC+8.

## `datasetLayout` (type: `string`):

Apify bills dataset writes per item. 'keyword' writes about 50x fewer items (cheaper). The flat CSV (OUTPUT.csv) is written in both layouts.

## Actor input object example

```json
{
  "keywords": [
    "content marketing",
    "seo audit"
  ],
  "geo": "",
  "timeRange": "today 12-m",
  "category": 0,
  "compareAcrossGroups": false,
  "includeRelatedQueries": false,
  "includeInterestByRegion": false,
  "trackChanges": false,
  "stateStoreName": "trends-state",
  "delayBetweenKeywordsMs": 8000,
  "delayBetweenRequestsMs": 2500,
  "maxRetries": 4,
  "baseDelayMs": 5000,
  "maxDelayMs": 120000,
  "hl": "en-US",
  "tz": -480,
  "datasetLayout": "flat"
}
```

# Actor output Schema

## `timeline` (type: `string`):

Default dataset, one row per keyword per date (overview view).

## `errors` (type: `string`):

Default dataset filtered to keyword, status, error (errors view).

## `datasetConsole` (type: `string`):

No description

## `csv` (type: `string`):

Flat CSV, one row per keyword per date.

## `runSummary` (type: `string`):

Counts, requests, 429s, charged events.

## `rescaleInfo` (type: `string`):

Anchor, factors, warnings and group membership. Present only when compareAcrossGroups is on.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "content marketing",
        "seo audit"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("raspberry_rondel/actor-google-trends").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "keywords": [
        "content marketing",
        "seo audit",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("raspberry_rondel/actor-google-trends").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "content marketing",
    "seo audit"
  ]
}' |
apify call raspberry_rondel/actor-google-trends --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,raspberry_rondel/actor-google-trends"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Ri5uCBgty8CW50u2B/builds/n2MhZZk37M3ZGACfp/openapi.json
