# Google Trends Scraper & Keyword Monitor (`magenta_courser/google-trends-monitor`) Actor

Enter keywords, get Google Trends interest over time, related queries and interest by region as flat rows, plus one summary row per keyword: latest value, change vs previous period, change since your last run, spike flag. No browser. Pay per keyword that returns data; no start fee.

- **URL**: https://apify.com/magenta_courser/google-trends-monitor.md
- **Developed by:** [SUNGHWAN CHO](https://apify.com/magenta_courser) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $9.00 / 1,000 keywords

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Scraper & Keyword Monitor

Enter a list of keywords and get **Google Trends data as flat rows**: interest over time, top and rising related queries, and interest by region, plus **one summary row per keyword** with the latest value, the change versus the previous period, the peak, and the change and spike flag since your last run.

This Google Trends scraper is built for people who look up the same keywords again and again: SEO and content teams, e-commerce sourcing, market research, trend reports in n8n or Make, and AI agents. It works with plain HTTP requests, so there is no browser to wait for, and you pay per keyword instead of per row.

### What you get

For every keyword and country (geo):

- **A summary row** (`keywordSummary`, free): latest value, average of the last 7 and 30 days, change versus the previous period in percent, peak value and time, change since your last run, a `spike` flag, the top rising related query, and a clear `status`.
- **Interest over time** (`timelinePoint`): one row per point in time, from per-minute data for the past hour to monthly data back to 2004.
- **Related queries** (`relatedQuery`): up to 25 top and 25 rising searches, with growth in percent, a `isBreakout` flag and a flag for queries that are new since your last run.
- **Interest by region** (`regionInterest`): one row per country, state / province or metro area.
- **A run summary** (`RUN_SUMMARY`, free): status, stop reason and charge flag of every keyword.

All rows are flat, with English field names. Numbers are numbers, missing values are `null`, times are ISO 8601 in UTC.

### Why this Google Trends scraper

- **No browser.** The Actor calls the same JSON endpoints the Google Trends website uses and runs in 256 MB of memory. In our tests a run with one keyword took 16 to 24 seconds and a run with 60 keywords took about 3 minutes 20 seconds.
- **Per-keyword pricing, no start fee.** One event per keyword, whatever you include. A time series with 270 points costs the same as one with 30. A run with a single keyword costs one keyword.
- **Failed keywords are free.** You pay only for a keyword that completed and has data. Failed, partial, skipped and no-data keywords are stored with their status but not charged.
- **Change since your last run.** The Actor remembers the last value of every keyword and reports what changed, on the same scale, with a spike flag. You do not have to store and compare the results yourself.
- **Handles HTTP 429 for you.** Every keyword runs on a fresh proxy session. A rate-limited request is retried alone, and a keyword that still fails is tried once more at the end of the run.
- **Honest status.** Every keyword gets a `status` and a `stopReason`. If Google changes its response format, the Actor reports `unexpectedResponse` instead of storing broken data.

### Use cases

- **SEO and content planning** - track your target keywords every day or week, and collect rising related queries as topic ideas before they peak.
- **E-commerce and product sourcing** - watch demand for product keywords by country and catch spikes early (`spike: true`).
- **Brand and competitor monitoring** - compare up to 5 brands on one scale and follow how the gap moves.
- **Market research by country** - run the same keywords in `US,GB,DE,KR,JP` and see where interest is highest, down to state or province.
- **Trend reports and alerts** - schedule a run, then send the summary rows to Google Sheets, Slack or email with n8n, Make or Zapier.
- **AI agents** - an agent that needs "is this keyword rising?" gets one small summary row per keyword instead of hundreds of data points.
- **A pytrends replacement** - keep your keyword lists and time range strings and move the rate-limit handling out of your code.

### Quick start

1. Open the Actor, enter one or more keywords, and pick a country and a time range.
2. Click **Start**. Results appear in the **Output** tab while the run is still going.
3. Export the dataset as JSON, CSV or Excel, or read it through the Apify API. Add a schedule to repeat the run every day or week.

### Input

Only `keywords` is required.

| Field | Default | What it does |
|---|---|---|
| `keywords` | required | Search terms in any language, one per item, up to 100 characters each. Duplicates are removed. |
| `geo` | `worldwide` | `worldwide` or a two-letter country code (`US`, `GB`, `DE`, `KR`, `JP`, `TW`, ...). Several countries: `"US,KR,JP"` - every keyword is looked up in every country. A region code such as `US-CA` also works. |
| `timeRange` | `today 3-m` | `now 1-H`, `now 4-H`, `now 1-d`, `now 7-d`, `today 1-m`, `today 3-m`, `today 12-m`, `today 5-y`, `all`. |
| `include` | `["interestOverTime"]` | Which detail rows to output: `interestOverTime`, `relatedQueries`, `interestByRegion`. The summary row is always output. `[]` = summary rows only. |
| `compareMode` | `false` | `false`: each keyword has its own 0-100 scale. `true`: keywords are sent in groups of up to 5 (in list order) and share one scale. See [Scales](#scales-read-this-before-comparing-numbers). |
| `customTimeRange` | - | `"YYYY-MM-DD YYYY-MM-DD"` (start end). Overrides `timeRange`. |
| `category` | `"0"` | Google Trends top-level category ID, for example `"7"` Finance, `"18"` Shopping, `"71"` Food & Drink. The full list is in the input form. |
| `historyStoreName` | `google-trends-monitor-history` | Named key-value store that remembers the last value of each keyword. Set to `""` to turn off "change since last run". |
| `spikeThresholdPct` | `50` | `spike` becomes `true` when the latest value is this many percent (and at least 10 points) above the baseline. |
| `proxyMode` | `auto` | `auto` (datacenter first, residential for retries and when datacenter IPs are rate-limited), `datacenter`, `residential`. |
| `maxRequests` | automatic | Upper limit of HTTP requests in the run, retries included. |

The resolution follows the time range:

| `timeRange` | Meaning | One point per | Points per keyword |
|---|---|---|---|
| `now 1-H` | Past hour | minute | about 60 |
| `now 4-H` | Past 4 hours | minute | about 240 |
| `now 1-d` | Past day | 8 minutes | about 180 |
| `now 7-d` | Past 7 days | hour | about 169 |
| `today 1-m` | Past 30 days | day | about 31 |
| `today 3-m` | Past 90 days | day | about 93 |
| `today 12-m` | Past 12 months | week | about 53 |
| `today 5-y` | Past 5 years | week | about 262 |
| `all` | 2004 to now | month | about 274 |

#### Example 1: daily keyword monitoring

Schedule this once a day. From the second run on, every summary row shows the change since the last run.

```json
{
  "keywords": ["bitcoin", "nvidia", "다이소"],
  "geo": "US",
  "timeRange": "today 3-m"
}
```

Keywords can be in any language. `다이소` is Daiso, a Korean retail chain.

#### Example 2: rising related queries for content ideas

```json
{
  "keywords": ["protein powder", "creatine"],
  "geo": "US",
  "timeRange": "today 1-m",
  "include": ["relatedQueries"]
}
```

#### Example 3: compare brands on one scale

Up to 5 keywords per group share one 0-100 scale. `include: []` returns only the summary rows.

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

#### Example 4: several countries with regional breakdown

Every keyword is looked up (and billed) once per country: 2 keywords x 3 countries = 6 lookups.

```json
{
  "keywords": ["k-beauty", "올리브영"],
  "geo": "US,JP,KR",
  "timeRange": "today 12-m",
  "category": "44",
  "include": ["interestOverTime", "interestByRegion"]
}
```

`올리브영` is Olive Young, a Korean beauty retailer. Category `"44"` is Beauty & Fitness.

### Output

One dataset, flat rows. The `type` field tells the rows apart. Every row has `keyword`, `geo`, `timeRange`, `category`, `url` (the same query on the Google Trends website), `scrapedAt`, `source: "google_trends"` and `schemaVersion`.

All examples below are copied from real runs on the Apify platform (2026-10-01 UTC).

#### `keywordSummary` - one per keyword and geo (free)

This row is from the second of two real runs, six minutes apart, with the input `{"keywords": ["bitcoin", "nvidia"], "geo": "US", "timeRange": "now 1-d", "include": ["relatedQueries"]}`. Because it is a second run, the "since last run" fields are filled.

```json
{
  "type": "keywordSummary",
  "keyword": "bitcoin",
  "geo": "US",
  "timeRange": "now 1-d",
  "category": 0,
  "scale": "single",
  "comparedWith": null,
  "status": "completed",
  "stopReason": "completed",
  "errorMessage": null,
  "charged": true,
  "hasData": true,
  "pointCount": 181,
  "resolution": "minute",
  "latestValue": 39,
  "latestTime": "2026-10-01T18:32:00.000Z",
  "partialValue": 41,
  "partialTime": "2026-10-01T18:40:00.000Z",
  "avg7Days": null,
  "avg30Days": null,
  "comparePeriod": "4h",
  "changeVsPreviousPeriodPct": -16.45,
  "peakValue": 100,
  "peakTime": "2026-10-01T09:44:00.000Z",
  "previousRunAt": "2026-10-01T18:37:23.827Z",
  "previousLatestValue": 42,
  "valueAtPreviousRun": 40,
  "changeSinceLastRun": -1,
  "changeSinceLastRunPct": -2.5,
  "spike": false,
  "spikeBasis": "previousRun",
  "relatedTopCount": 25,
  "relatedRisingCount": 9,
  "breakoutQueryCount": 2,
  "topRisingQuery": "michael saylor bitcoin market growth",
  "topRisingQueryValue": 14250,
  "newRisingQueryCount": 2,
  "regionCount": null,
  "topRegionName": null,
  "interestOverTimeStatus": "ok",
  "relatedQueriesStatus": "ok",
  "interestByRegionStatus": "notRequested",
  "requests": 6,
  "retries": 1,
  "url": "https://trends.google.com/trends/explore?date=now%201-d&geo=US&q=bitcoin&hl=en-US",
  "scrapedAt": "2026-10-01T18:43:55.143Z",
  "source": "google_trends",
  "schemaVersion": 1
}
```

- `latestValue` is the latest **complete** point. Google's unfinished last point (`isPartial`) is reported separately as `partialValue` and is never used for averages, changes or the peak.
- `changeVsPreviousPeriodPct` compares the mean of the last period with the period before it. `comparePeriod` says how long that period is: `15m` for the past hour, `1h` for 4 hours, `4h` for the past day, `1d` for 7 days, `7d` for 1 to 3 months, `28d` for 12 months, `91d` for 5 years, `365d` for `all`.
- `avg7Days` and `avg30Days` are `null` when the series is shorter than 7 or 30 days.
- `status`: `completed` (everything you asked for was saved), `partial` (the time series was saved, another part failed), `failed`, `budgetLimited` (your maximum charge, `maxRequests` or the run timeout was reached), `notStarted`.
- `stopReason`: `completed`, `noData`, `rateLimited`, `invalidRequest`, `unexpectedResponse`, `httpError`, `networkError`, `chargeLimitReached`, `requestBudget`, `timeLimit`, `blocked`, `datasetWriteFailed`.
- `charged` is `true` only when `status` is `completed` and `hasData` is `true`.

In compare mode the summary row also says what the keyword was compared with. From a real run of Example 3 (fields shortened):

```json
[
  {"type": "keywordSummary", "keyword": "chatgpt", "geo": "US", "timeRange": "today 12-m", "scale": "compared", "comparedWith": "gemini | claude", "status": "completed", "charged": true, "latestValue": 73, "changeVsPreviousPeriodPct": 9.12, "peakValue": 100, "peakTime": "2025-11-02T00:00:00.000Z"},
  {"type": "keywordSummary", "keyword": "gemini", "geo": "US", "timeRange": "today 12-m", "scale": "compared", "comparedWith": "chatgpt | claude", "status": "completed", "charged": true, "latestValue": 32, "changeVsPreviousPeriodPct": 20.56, "peakValue": 35, "peakTime": "2026-04-12T00:00:00.000Z"},
  {"type": "keywordSummary", "keyword": "claude", "geo": "US", "timeRange": "today 12-m", "scale": "compared", "comparedWith": "chatgpt | gemini", "status": "completed", "charged": true, "latestValue": 21, "changeVsPreviousPeriodPct": 0, "peakValue": 32, "peakTime": "2026-03-01T00:00:00.000Z"}
]
```

A keyword without enough search volume completes with `stopReason: "noData"` and is not charged. From a real run with a random string as the keyword (fields shortened):

```json
{
  "type": "keywordSummary",
  "keyword": "zqxjvkwpl mnbtrcyh 83615 qqfdz",
  "geo": "TW",
  "timeRange": "today 1-m",
  "status": "completed",
  "stopReason": "noData",
  "errorMessage": null,
  "charged": false,
  "hasData": false,
  "pointCount": 0,
  "latestValue": null,
  "interestOverTimeStatus": "ok",
  "requests": 5,
  "retries": 1
}
```

#### `timelinePoint` - one per point in time (`include: interestOverTime`)

```json
[
  {"type": "timelinePoint", "keyword": "다이소", "geo": "KR", "timeRange": "today 3-m", "category": 0, "scale": "single", "comparedWith": null, "time": "2026-09-30T00:00:00.000Z", "value": 21, "hasData": true, "isPartial": false, "url": "https://trends.google.com/trends/explore?date=today%203-m&geo=KR&q=%EB%8B%A4%EC%9D%B4%EC%86%8C&hl=en-US", "scrapedAt": "2026-10-01T18:38:29.972Z", "source": "google_trends", "schemaVersion": 1},
  {"type": "timelinePoint", "keyword": "다이소", "geo": "KR", "timeRange": "today 3-m", "category": 0, "scale": "single", "comparedWith": null, "time": "2026-10-01T00:00:00.000Z", "value": 21, "hasData": true, "isPartial": true, "url": "https://trends.google.com/trends/explore?date=today%203-m&geo=KR&q=%EB%8B%A4%EC%9D%B4%EC%86%8C&hl=en-US", "scrapedAt": "2026-10-01T18:38:29.972Z", "source": "google_trends", "schemaVersion": 1}
]
```

Times are UTC. The last point of a series is usually unfinished (`isPartial: true`). `hasData: false` marks points where Google has not enough data; their `value` is 0, exactly as on Google's chart.

#### `relatedQuery` - top and rising related searches (`include: relatedQueries`)

```json
[
  {"type": "relatedQuery", "keyword": "bitcoin", "geo": "US", "timeRange": "now 1-d", "category": 0, "kind": "top", "rank": 1, "query": "bitcoin price", "value": 100, "formattedValue": "100", "isBreakout": false, "isNewSinceLastRun": null, "queryUrl": "https://trends.google.com/trends/explore?q=bitcoin+price&date=now+1-d&geo=US", "url": "https://trends.google.com/trends/explore?date=now%201-d&geo=US&q=bitcoin&hl=en-US", "scrapedAt": "2026-10-01T18:43:55.143Z", "source": "google_trends", "schemaVersion": 1},
  {"type": "relatedQuery", "keyword": "bitcoin", "geo": "US", "timeRange": "now 1-d", "category": 0, "kind": "rising", "rank": 1, "query": "michael saylor bitcoin market growth", "value": 14250, "formattedValue": "Breakout", "isBreakout": true, "isNewSinceLastRun": false, "queryUrl": "https://trends.google.com/trends/explore?q=michael+saylor+bitcoin+market+growth&date=now+1-d&geo=US", "url": "https://trends.google.com/trends/explore?date=now%201-d&geo=US&q=bitcoin&hl=en-US", "scrapedAt": "2026-10-01T18:43:55.143Z", "source": "google_trends", "schemaVersion": 1},
  {"type": "relatedQuery", "keyword": "bitcoin", "geo": "US", "timeRange": "now 1-d", "category": 0, "kind": "rising", "rank": 5, "query": "goog stock", "value": 100, "formattedValue": "+100%", "isBreakout": false, "isNewSinceLastRun": true, "queryUrl": "https://trends.google.com/trends/explore?q=goog+stock&date=now+1-d&geo=US", "url": "https://trends.google.com/trends/explore?date=now%201-d&geo=US&q=bitcoin&hl=en-US", "scrapedAt": "2026-10-01T18:43:55.143Z", "source": "google_trends", "schemaVersion": 1}
]
```

Up to 25 `top` and 25 `rising` queries per keyword. For `top`, `value` is 0-100 relative to the most searched related query. For `rising`, `value` is the growth in percent; when Google shows "Breakout" instead of a percentage, `isBreakout` is `true`. `isNewSinceLastRun` is `null` on the first run and for `top` queries.

#### `regionInterest` - one per country / region with data (`include: interestByRegion`)

```json
{
  "type": "regionInterest",
  "keyword": "다이소",
  "geo": "KR",
  "timeRange": "today 3-m",
  "category": 0,
  "rank": 1,
  "regionCode": "KR-27",
  "regionName": "Daegu",
  "value": 100,
  "url": "https://trends.google.com/trends/explore?date=today%203-m&geo=KR&q=%EB%8B%A4%EC%9D%B4%EC%86%8C&hl=en-US",
  "scrapedAt": "2026-10-01T18:38:29.972Z",
  "source": "google_trends",
  "schemaVersion": 1
}
```

Countries for `worldwide`, states / provinces for a country, metro areas for a region code. Regions without enough data are left out. Region names are in English.

#### Run summary (free)

The key-value store record `RUN_SUMMARY` lists every keyword with `status`, `stopReason`, `charged`, requests and retries, plus totals, rows stored per type, proxy fallbacks and whether your charge limit was reached. From a real run with two keywords, one of them without data (shortened):

```json
{
  "source": "google_trends",
  "schemaVersion": 1,
  "runStatus": "succeeded",
  "stopReason": "completed",
  "totals": {"keywords": 2, "completed": 2, "partial": 0, "failed": 0, "budgetLimited": 0, "notStarted": 0, "noData": 1, "charged": 1},
  "rowsStored": {"timelinePoint": 31, "relatedQuery": 0, "regionInterest": 0, "keywordSummary": 2},
  "charging": {"eventName": "keyword", "chargedEvents": 1, "limitReached": false, "isPayPerEvent": false, "maxTotalChargeUsd": null},
  "proxy": {"mode": "auto", "residentialFallbacks": 0},
  "keywords": [
    {"keyword": "zqxjvkwpl mnbtrcyh 83615 qqfdz", "geo": "TW", "status": "completed", "stopReason": "noData", "charged": false, "requests": 5, "retries": 1, "baseline": "promoted", "errorMessage": null},
    {"keyword": "nvidia", "geo": "TW", "status": "completed", "stopReason": "completed", "charged": true, "requests": 5, "retries": 1, "baseline": "promoted", "errorMessage": null}
  ]
}
```

The run fails only when no keyword could be saved. If some keywords fail, the others are kept; read `RUN_SUMMARY` and re-run only the failed ones.

#### Dataset views

The **Output** tab has five table views: **All rows**, **Keyword summaries**, **Interest over time**, **Related queries** and **Interest by region**. Views choose columns, not rows - to get only one row type from the API, filter on `type` in your own code, or request only what you need with `include`.

### Change since your last run

With `historyStoreName` set (the default), the Actor remembers the latest complete point of every keyword + geo + time range + category. On the next run the summary row shows:

- `previousRunAt`, `previousLatestValue` - what the last run saw.
- `valueAtPreviousRun` - the value **in today's series** at the time of the last run's latest point. Because Google rescales every response, this is the number to compare with; `changeSinceLastRun` and `changeSinceLastRunPct` use it whenever that point is still inside the series.
- `spike` - `true` when the latest value is at least `spikeThresholdPct` percent and 10 points above that baseline. On a first run the baseline is the previous period (`spikeBasis: "previousPeriod"`).
- `newRisingQueryCount`, and `isNewSinceLastRun` on rising `relatedQuery` rows.

The baseline only moves after the rows of a keyword were stored, so a failed run never hides a change. Use different store names to keep separate schedules apart.

In the summary example above, the previous run stored 42 as its latest value (`previousLatestValue`). Six minutes later Google reports that same point as 40 in the new series (`valueAtPreviousRun`), and the new latest value is 39. The change is therefore -1 (-2.5%), not -3. Two rising queries were not in the previous list (`newRisingQueryCount: 2`).

### Scales: read this before comparing numbers

Google Trends does not return search volumes. Every value is **relative, 0-100**:

- **Default (`compareMode: false`)**: 100 is the keyword's own peak within the chosen geo and time range. A 60 for "bitcoin" and a 60 for "nvidia" say nothing about which one is searched more.
- **`compareMode: true`**: keywords are sent in groups of up to 5, in list order. Inside a group, 100 is the peak of the most searched keyword, so values of the group are comparable (`scale: "compared"`, `comparedWith` lists the others). Values from different groups are not comparable. Google allows at most 5 keywords per comparison. If the last group has a single keyword, it is a normal single lookup.
- The scale changes whenever the time range, the geo or the group changes - and also between runs, when a new peak appears. That is why `valueAtPreviousRun` exists.
- Related queries and interest by region are always per keyword, also in compare mode.

### Pricing

Pay per event: **$0.01 for each keyword x geo that completed with data** (event name `keyword`). There is no start fee and no charge per dataset row. Proxy traffic, retries, summary rows and the run summary are included.

| What you run | Billed keywords | Cost |
|---|---|---|
| 1 keyword, once | 1 | $0.01 |
| 1 keyword, every day for 30 days | 30 | $0.30 |
| 5 keywords, every day for 30 days | 150 | $1.50 |
| 20 keywords, once a week for 4 weeks | 80 | $0.80 |
| 100 keywords, once | 100 | $1.00 |
| 5 keywords in 3 countries, every day for 30 days | 450 | $4.50 |

The price is the same for every time range and for any combination of `include`.

**Not charged:**

- Keywords with status `failed`, `partial`, `budgetLimited` or `notStarted`.
- Keywords that complete without data (`hasData: false`, `stopReason: "noData"`).
- Dataset rows, summary rows and the run summary.

**Good to know:**

- In compare mode every keyword of a completed group that has data is one event.
- A keyword looked up in 3 countries is 3 events.
- If you set a maximum cost per run, the Actor only fetches keywords that still fit the budget and marks the rest `budgetLimited`. It never fetches a keyword it cannot bill and never bills a keyword it did not store.
- `RUN_SUMMARY.totals.charged` always equals the number of summary rows with `charged: true`.

### Reliability: what we measured

How the Actor deals with Google's rate limits:

1. Every keyword starts on a fresh proxy session with its own cookie and fixed browser headers.
2. A request that Google refuses (HTTP 429) is retried alone: first with the same token on a new session, then after a pause with new tokens.
3. A keyword that still fails is tried once more at the end of the run, without fetching again what was already saved.
4. In `auto` mode datacenter proxies are used first; retries and rate-limited stretches go through residential proxies.
5. After 30 refused requests in a row the run stops instead of hammering Google.

What we measured, **on one day (2026-10-02)**:

- One run of 120 keywords (90 English, 30 Korean / Japanese / Chinese, over 9 geos and 7 time ranges): 120 of 120 lookups completed. 19 of them needed at least one retry. The run took 7 minutes 18 seconds.
- Seven later runs with 118 lookups in total: 118 of 118 completed.

This is a one-day sample, taken while our datacenter IPs were rate-limited, so almost all requests went through residential proxies. It is a measurement, not a promised success rate. Google can tighten its limits at any time. Check `status` in your own runs - and remember that a keyword that fails costs you nothing.

### Limits and honest notes

- **Relative values only.** 0-100, rescaled on every request (see [Scales](#scales-read-this-before-comparing-numbers)). No absolute search volume.
- **Numbers move between requests.** Google Trends works on a sample of searches and rescales each response, so the same day can show a slightly different value tomorrow.
- **Compare mode: 5 keywords per group** - Google's limit.
- **Related topics are not offered.** In our tests Google returned an empty list for them on this path.
- **"Trending now" is not included.** This Actor is for keywords you choose.
- **Web search only.** Image, News, Shopping and YouTube search are not offered.
- **Top-level categories only** (the 25 in the input form).
- **Rate limits are Google's and can change.** Google rate-limits these endpoints (HTTP 429) and blocked our small datacenter proxy pool after about a hundred keywords. The retries and the residential fallback are there for that; if Google still refuses, the keyword is `failed` with `stopReason: "rateLimited"` and is not charged.
- **This is not an official API.** Google can change the response format at any time. The Actor checks the format of every response and reports `unexpectedResponse` rather than storing unchecked data.
- **Low-volume keywords and short time ranges often have no data** (`hasData: false`). Google sometimes returns a series with a single non-zero point for a very rare term; that counts as data.
- **Largest run we tested: 120 keywords.** For longer lists, split the keywords over several runs.
- **Verified in tests:** geo `worldwide`, `US`, `GB`, `DE`, `CA`, `AU`, `KR`, `JP`, `TW`, `US-CA`; all time ranges in the table; custom date ranges; all 25 top-level categories; Korean, Japanese and Chinese keywords.

### Use from the API, schedules and automation tools

Run and get the rows in one call:

```bash
curl -X POST "https://api.apify.com/v2/acts/magenta_courser~google-trends-monitor/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords": ["bitcoin", "nvidia"], "geo": "US", "timeRange": "now 7-d", "include": []}'
```

Python (`pip install apify-client`):

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")
run = client.actor("magenta_courser/google-trends-monitor").call(run_input={
    "keywords": ["bitcoin", "nvidia"],
    "geo": "US",
    "timeRange": "today 3-m",
    "include": ["interestOverTime", "relatedQueries"],
})
rows = client.dataset(run["defaultDatasetId"]).list_items().items
summaries = [r for r in rows if r["type"] == "keywordSummary"]
timeline = [r for r in rows if r["type"] == "timelinePoint"]
```

- **Schedules** - create a schedule in Apify Console (for example every day at 06:00) with a fixed input. Keep the same `historyStoreName` so each run is compared with the one before.
- **n8n, Make, Zapier** - use the Apify integration: run the Actor, read the dataset items, keep the rows with `type = keywordSummary`, and branch on `spike = true` to send an alert.
- **Google Sheets / BI tools** - export the dataset as CSV or Excel, or load the JSON from the dataset API URL.

### Use with AI agents (MCP)

The Actor works as a tool for AI agents through the [Apify MCP server](https://mcp.apify.com). Add it to an MCP client such as Claude Desktop, Cursor or VS Code:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=magenta_courser/google-trends-monitor",
      "headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }
    }
  }
}
```

Then ask, for example: *"Check Google Trends for 'air fryer' and 'rice cooker' in the US over the last 90 days. Which one is rising?"*

Tips for agents:

- Pass `"include": []` to get only one summary row per keyword. That is the smallest answer.
- Read `status` and `hasData` first, then `latestValue`, `changeVsPreviousPeriodPct`, `changeSinceLastRunPct`, `spike` and `topRisingQuery` (needs `"include": ["relatedQueries"]`).
- Values are relative (0-100). To say which of several keywords is searched more, set `"compareMode": true` with up to 5 keywords.
- `geo` takes ISO country codes (`"US"`, `"KR"`), not country names.

### Moving from pytrends

pytrends was archived in April 2025 and is no longer maintained. If your scripts stopped working or keep running into HTTP 429, the same requests map to this Actor like this:

| pytrends | This Actor |
|---|---|
| `build_payload(kw_list, timeframe, geo, cat)` | `keywords`, `timeRange` (same strings: `today 3-m`, `now 7-d`, `today 5-y`, `all`), `geo`, `category` |
| `timeframe="2026-01-01 2026-06-30"` | `customTimeRange: "2026-01-01 2026-06-30"` |
| `interest_over_time()` | `include: ["interestOverTime"]` - `timelinePoint` rows |
| `related_queries()` | `include: ["relatedQueries"]` - `relatedQuery` rows with `kind` `top` / `rising` |
| `interest_by_region()` | `include: ["interestByRegion"]` - `regionInterest` rows |
| 5 keywords in one `kw_list` (shared scale) | `compareMode: true` |
| one keyword per `kw_list` (own scale) | default (`compareMode: false`) |
| `related_topics()`, `trending_searches()`, `suggestions()` | not offered |

Differences to know: rows are long format (one row per keyword and point) instead of a wide DataFrame - in pandas, `pd.DataFrame(timeline).pivot(index="time", columns="keyword", values="value")` gives the wide table. Proxies, cookies and retries are handled by the Actor.

### FAQ

**Is this the official Google Trends API?**
No. Google's own Trends API is in a limited alpha that you have to apply for. This Actor reads the public data behind the Google Trends website. It is not affiliated with Google.

**Do I need a Google account, an API key or my own proxies?**
No. You only need an Apify account. Proxies are included in the price.

**Are the values search volumes?**
No. Google Trends only publishes relative interest from 0 to 100. See [Scales](#scales-read-this-before-comparing-numbers).

**Why is `latestValue` different from what I saw yesterday for the same day?**
Google rescales the series on every request and works on a sample of searches. Compare with `valueAtPreviousRun`, which is read from today's series.

**Why did a keyword fail?**
Read `stopReason` and `errorMessage` in its summary row. `rateLimited`: run it again in a few minutes. `invalidRequest`: Google rejected the geo, time range or category. `unexpectedResponse`: Google changed something - please open an issue. Failed keywords are not charged.

**What does `partial` mean?**
The time series was saved, but related queries or regions kept failing. You keep the rows and the keyword is not charged. Run it again to get the missing part.

**Am I charged for a keyword that has no data?**
No. It is stored with `hasData: false` and `stopReason: "noData"`, and `charged` is `false`.

**Can I monitor hourly?**
Yes. Use `now 1-d` or `now 7-d` and an hourly schedule. Keep the same `historyStoreName`.

**Can I compare more than 5 keywords?**
Google compares at most 5 at a time. With more keywords, the Actor makes groups of 5 in list order, and values of different groups are not comparable. To put more keywords on one scale, use one run per group, put the same anchor keyword first in every group, and rescale the groups by the anchor's values.

**How do I get only the rising queries?**
Use `"include": ["relatedQueries"]` and keep the rows with `type = "relatedQuery"` and `kind = "rising"`.

**Which time zone are the times in?**
UTC. Daily points are UTC days.

**How many keywords can I put in one run?**
There is no fixed limit. The largest run we tested had 120 keywords and took about 7 minutes. For long lists, split them into several runs.

**Something is wrong or missing. Where do I report it?**
Open an issue on the Actor's **Issues** tab. Include the run ID.

### Notes

This Actor collects only public, aggregated search interest data from Google Trends. It does not log in to any account and does not collect personal data. It does not solve captchas. Google Trends is a trademark of Google LLC; this Actor is an independent tool and is not affiliated with or endorsed by Google.

# Actor input Schema

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

Search terms to look up on Google Trends, one per item, in any language. Example: \["bitcoin", "nvidia", "다이소"] (다이소 = Daiso, a Korean retail chain). Write the term as people type it into Google; do not add operators or quotes. Max 100 characters per keyword; duplicates are removed. Each keyword is looked up on its own 0-100 scale unless "Compare keywords" is on. You pay one "keyword" event per keyword and geo that completes with data.

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

Where the searches come from. "worldwide" (default) or a two-letter ISO country code in upper case: "US", "GB", "DE", "KR" (South Korea), "JP" (Japan), "TW" (Taiwan). Several countries: one string separated by commas, e.g. "US,KR,JP" - every keyword is then looked up (and billed) once per country. A region code such as "US-CA" (California) also works. Country names such as "Korea" are not accepted.

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

How far back to look. Use exactly one of: "now 1-H" (past hour), "now 4-H" (past 4 hours), "now 1-d" (past day), "now 7-d" (past 7 days), "today 1-m" (past 30 days), "today 3-m" (past 90 days, default), "today 12-m" (past 12 months), "today 5-y" (past 5 years), "all" (2004 - present). The range decides the resolution: past hour / 4 hours = per minute, past day = 8 minutes, past 7 days = hourly, past 30 / 90 days = daily, past 12 months / 5 years = weekly, all = monthly. For daily monitoring use "today 1-m" or "today 3-m"; for hourly monitoring "now 7-d".

## `include` (type: `array`):

Which detail rows to output, as an array. "interestOverTime" (default) = one row per point in time. "relatedQueries" = top and rising related searches (up to 25 each per keyword). "interestByRegion" = one row per country / region with data. Example: \["interestOverTime", "relatedQueries"]. The keywordSummary row (latest value, changes, spike flag) is always output; pass an empty array \[] to get only summary rows - the smallest output, good for AI agents. The price per keyword is the same whatever you include.

## `compareMode` (type: `boolean`):

false (default): every keyword gets its own 0-100 scale, where 100 is that keyword's own peak - use this for monitoring. true: keywords are sent to Google in groups of up to 5 (in list order) and share one scale, where 100 is the peak of the most searched keyword in the group - use this to see which keyword is searched more. Google allows at most 5 keywords per comparison; values from different groups, or from compare on and off, are not comparable.

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

Optional. Two dates as "YYYY-MM-DD YYYY-MM-DD" (start, space, end), for example "2026-01-01 2026-06-30". The start must be before the end. When set, it overrides "Time range".

## `category` (type: `string`):

Optional Google Trends category ID as a string, to narrow an ambiguous word such as "apple" or "jaguar". "0" = all categories (default). Examples: "7" Finance, "18" Shopping, "71" Food & Drink, "44" Beauty & Fitness, "5" Computers & Electronics, "67" Travel. Only the listed top-level categories are accepted.

## `historyStoreName` (type: `string`):

Name of the key-value store that remembers the latest value of every keyword, so the next run can report the change since your last run and flag spikes. Letters, digits and hyphens, 3-63 characters, e.g. "my-brand-keywords-daily". Runs that use the same name share one history; use different names for separate schedules. Set to an empty string "" to turn the comparison off. Default: "google-trends-monitor-history".

## `spikeThresholdPct` (type: `integer`):

The summary row gets spike = true when the latest value is at least this many percent - and at least 10 points - above the value at your last run (on a first run: above the previous period). Integer from 1 to 1000. Example: 50 flags a rise from 40 to 60 or more. Default 50.

## `proxyMode` (type: `string`):

"auto" (default, recommended): datacenter proxies first; retries and any rate-limited stretch go through residential proxies. "datacenter": datacenter proxies only - Google rate-limits a small proxy pool after about 100 keywords. "residential": every request through residential proxies. Proxy traffic is included in the price per keyword.

## `maxRequests` (type: `integer`):

Optional safety limit: the maximum number of HTTP requests in this run, retries included (integer, minimum 10, e.g. 200). Leave empty for an automatic limit of about 3x what the keywords need. Keywords that do not fit get status "budgetLimited" and are not charged.

## `proxyConfiguration` (type: `object`):

Optional, advanced. Only used when you pick specific Apify Proxy groups or enter your own proxy URLs here; it then replaces "Proxy mode". Leave empty in normal use.

## Actor input object example

```json
{
  "keywords": [
    "bitcoin"
  ],
  "geo": "worldwide",
  "timeRange": "today 3-m",
  "include": [
    "interestOverTime"
  ],
  "compareMode": false,
  "category": "0",
  "historyStoreName": "google-trends-monitor-history",
  "spikeThresholdPct": 50,
  "proxyMode": "auto"
}
```

# Actor output Schema

## `rows` (type: `string`):

Every row as dataset items. The "type" field tells them apart: keywordSummary, timelinePoint, relatedQuery, regionInterest.

## `summaries` (type: `string`):

The same dataset shown with the core columns of the keywordSummary rows (one per keyword and geo): status, latest value, changes, spike flag, charged.

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

Free key-value record: status, stop reason and charge flag for every keyword, totals (completed, noData, charged), request and retry counts, proxy fallback and charge limit.

# 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": [
        "bitcoin"
    ],
    "geo": "worldwide"
};

// Run the Actor and wait for it to finish
const run = await client.actor("magenta_courser/google-trends-monitor").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": ["bitcoin"],
    "geo": "worldwide",
}

# Run the Actor and wait for it to finish
run = client.actor("magenta_courser/google-trends-monitor").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": [
    "bitcoin"
  ],
  "geo": "worldwide"
}' |
apify call magenta_courser/google-trends-monitor --silent --output-dataset

```

## MCP server setup

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

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/xCij6QqRaie4fCEg9/builds/fp61TZPPFXe8SFvfe/openapi.json
