# Google Trends Scraper — Interest Over Time & By Region (`scrapersdelight/google-trends-interest-scraper`) Actor

Scrape Google Trends search interest as flat, spreadsheet-ready rows: one per keyword and date, one per keyword and region. Any country or subregion, any timeframe from the last hour back to 2004, and web / news / YouTube / image / shopping search. No browser, no API key, no login.

- **URL**: https://apify.com/scrapersdelight/google-trends-interest-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** SEO tools, Marketing, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.05 / 1,000 per interest row returneds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Google Trends Scraper — Interest Over Time & By Region

Search interest from [Google Trends](https://trends.google.com), delivered **flat**: one row per search term and date, and one row per search term and region. Load it straight into a spreadsheet, a BI tool or a database — no unnesting, no JSON surgery.

### Why this one

Every other Google Trends actor hands you **one item per keyword with the raw Google JSON nested inside it**. Export that to CSV and the whole 12-month timeline arrives as an unusable blob in a single cell.

This one flattens it. One keyword over the past 12 months = **53 tidy rows**, each with an ISO date, the 0-100 interest value, and the series average alongside it so you can see at a glance whether a week was above or below par.

It also runs on **plain HTTP, no browser**. Measured 2026-09-02 on a live Apify run: 3 keywords × 2 countries, both slices, **483 rows in 18.8 seconds** and 24 HTTP requests. For comparison, Apify's own `google-trends-scraper` runs a headless browser at 4 GB, and its public 30-day run scoreboard on the same day read **5,348 TIMED-OUT and 442 FAILED out of 20,571 runs — 28% of runs did not succeed.**

### Quick start

```jsonc
{
  "searchTerms": ["bitcoin", "ethereum", "solana"],
  "geos": ["US", "GB"],
  "timeframe": "today 12-m",
  "includeInterestOverTime": true,
  "includeInterestByRegion": true
}
```

Every term is scraped once per location, so 3 terms × 2 countries = 6 queries.

### Output

Two row shapes in one dataset, told apart by `dataType`. **27 fields**, same columns on every row so a CSV export stays rectangular.

#### `interest_over_time` — the line chart, flattened

```json
{
  "searchTerm": "ethereum",
  "searchTermType": "Search term",
  "dataType": "interest_over_time",
  "geo": "US",
  "timeframe": "today 12-m",
  "timeframeLabel": "Past 12 months",
  "timeRange": "2025-09-02 2026-09-02",
  "granularity": "WEEK",
  "category": 0,
  "searchProperty": "web",
  "date": "2025-08-31T00:00:00.000Z",
  "timestamp": 1756598400,
  "formattedTime": "Aug 31 – Sep 6, 2025",
  "value": 74,
  "formattedValue": "74",
  "hasData": true,
  "isPartial": false,
  "seriesAverage": 58.8,
  "comparisonGroup": null,
  "trendsUrl": "https://trends.google.com/trends/explore?date=today+12-m&q=ethereum&geo=US&hl=en-US",
  "scrapedAt": "2026-09-02T04:41:50.757Z"
}
```

#### `interest_by_region` — the map, flattened

```json
{
  "searchTerm": "ethereum",
  "dataType": "interest_by_region",
  "geo": "US",
  "rank": 1,
  "regionCode": "US-WY",
  "regionName": "Wyoming",
  "regionLevel": "subregion",
  "value": 100,
  "formattedValue": "100",
  "hasData": true,
  "latitude": null,
  "longitude": null,
  "trendsUrl": "https://trends.google.com/trends/explore?date=today+12-m&q=ethereum&geo=US&hl=en-US"
}
```

#### Measured field fill

From the live Apify run above (483 rows, 3 terms × 2 countries):

| Row type | n | Fields at 100% fill |
| --- | --- | --- |
| `interest_over_time` | 318 | all 21 applicable fields — `date`, `timestamp`, `formattedTime`, `value`, `formattedValue`, `hasData`, `isPartial`, `seriesAverage`, `granularity`, `timeRange`, `timeframeLabel`, `geo`, `searchTerm`, `searchTermType`, `dataType`, `timeframe`, `category`, `searchProperty`, `trendsUrl`, `scrapedAt` |
| `interest_by_region` | 165 | all 19 applicable fields — `regionCode`, `regionName`, `regionLevel`, `rank`, `value`, `formattedValue`, `hasData`, plus the same shared columns |

The offline regression suite replays **33 captured Google Trends responses across 11 query shapes** (worldwide, country, US state, custom date range, hourly, per-minute, YouTube, News, a finance category, forced City detail, and a 3-way comparison) through this exact parser: **1,456 rows, 0 duplicates, 0 dropped records, every integrity assertion passing.**

Fields that are null by design, not by failure:

| Field | Where it fills | Why |
| --- | --- | --- |
| `date` / `timestamp` / `formattedTime` / `granularity` / `seriesAverage` | over-time rows only | a region has no date |
| `regionCode` / `regionName` / `regionLevel` / `rank` | region rows only | a date has no region |
| `latitude` / `longitude` | City detail only (2% of region rows) | Google returns coordinates instead of a region code for cities |
| `regionCode` | 98% of region rows | city rows have coordinates, not a code |
| `geo` | `""` means Worldwide | an empty string is the real Google Trends value for worldwide |
| `comparisonGroup` | comparison mode only | lists the terms a row was normalised against |

### The one thing you must understand about Google Trends numbers

The value is **0-100, relative, and scaled to the peak of whatever you asked for**. It is not a search volume.

- **By default each term is scraped on its own**, so its 100 is *its own* busiest period. Term A's 80 and term B's 80 are **not comparable**.
- Set **`compareKeywords: true`** and up to 5 terms go into ONE Google query, scaled to a single shared peak — then they *are* directly comparable ("brand A ran at 3× brand B all year"). Rows from a comparison carry the group in `comparisonGroup`.

Two more things the raw API will bite you with, both surfaced as fields here:

- **`isPartial: true`** marks the final period, which is still being collected. It almost always looks like a crash. Filter it out before you charting a trend line.
- **`hasData: false`** means Google has no data for that point — not an interest of zero.

### What you can control

| Input | What it does |
| --- | --- |
| `searchTerms` | The keywords. Each is one query per location. |
| `geos` | `""` = Worldwide, `US` / `GB` / `DE` = country, `US-CA` / `GB-ENG` = state or region. Pass several. |
| `timeframe` | Past hour → 2004-present. Google picks the granularity: per minute, per 8 minutes, hourly, daily, weekly or monthly. |
| `customTimeRange` | `2024-01-01 2024-12-31` — any window back to 2004. |
| `category` | A Google Trends topic ID (7 Finance, 45 Health, 174 Autos…) to strip out same-spelling searches from other subjects. |
| `searchProperty` | Web, **Images, News, YouTube or Google Shopping** — the same term measured on a different Google surface. |
| `regionResolution` | Automatic, or force **Metro** / **City** detail. |
| `compareKeywords` | Scale up to 5 terms against a single shared peak. |
| `includeRegionsWithNoData` | Keep the blank regions Google pads its map with (see below). |
| `maxItems` | Hard row cap; the run stops cleanly at it. |
| `maxConcurrency` | Queries in flight. Default 2 — see rate limits. |

#### Blank regions are dropped, and not charged

Google pads the region list with entries it explicitly flags as having no data. Measured 2026-09-02: a **worldwide** query returned 250 countries of which **54 carried data**, and forced **City** detail returned 200 cities of which **10 did**. Shipping — and billing you for — 190 blank city rows is not a result set, so they are dropped by default and the count is reported in the run status. `includeRegionsWithNoData` puts them back if you are drawing a map that needs every shape.

### Pricing

Pay per row. Nothing else — no run fee, no platform-usage surcharge on top.

| Event | Price | When it fires |
| --- | --- | --- |
| `trend-row` | **$0.00005** | One per interest row delivered to your dataset |

That is **$0.05 per 1,000 rows**. A full 12-month weekly series for one keyword is 53 rows = **$0.0027**; add interest by region for a US query and it is 104 rows = **$0.0052**. Tracking 500 keywords weekly, time series only, costs about **$1.33 a month**.

Rows are billed through Apify's gated `pushData`, so at a spend cap the run stops cleanly and you are never charged for rows you did not receive.

### Rate limits — the honest version

Google Trends rate-limits hard, and this is the only real difficulty in the lane. Measured 2026-09-02 over 20 keywords (US, past 12 months):

| Setup | First-attempt success |
| --- | --- |
| Fresh session per query, **1 at a time** | **18 / 20** |
| One shared session, 1 at a time | 15 / 20 |
| Fresh session per query, 4 at a time | 8 / 20 |

So the actor takes a **fresh cookie jar and a fresh proxy session for every query, and again for every retry**, backs off exponentially, and retries a rate-limited query up to four times. On the live 6-query Apify run above it absorbed **zero** 429s across 24 requests.

If a query is still rate-limited after all four attempts it is **counted and named in the run status message as RATE-LIMITED**, never silently reported as "this keyword has no data". Re-run those terms, or lower `maxConcurrency`.

Apify Proxy is on by default and should stay on — the session rotation is the entire defence. Any proxy group works; **no residential requirement was measured** (the same call returned 200 through Apify's automatic, datacenter, static-US and residential pools, and from an unproxied home IP).

### Notes / honest limits

- **No login, no API key, no browser.** Two HTTP hops per query against Google's own public Trends endpoints. A cookie-minting warm request comes first — without it Google answers **429 from every IP**, which is what makes this lane look harder than it is.
- **Related queries and related topics are not in this actor.** They are a different job (keyword ideation) with a different row shape, and mixing them into this dataset would wreck the flat CSV. They belong in a sibling actor.
- **This is relative interest, not search volume.** Google does not publish absolute query counts anywhere, and any tool claiming to give you them from Trends is modelling, not measuring.
- **Google re-samples.** Trends is built from a sample of searches, so the same query re-run tomorrow can differ by a point or two. That is Google, not the scraper.
- **`now 1-H` / `now 4-H` data has a short shelf life** — Google keeps the per-minute series for only the recent window, so schedule those runs rather than backfilling them.
- **Sub-country locations only go so deep.** A country gives states/provinces, a US state gives metro areas, and City detail returns Google's top 200 cities — most of which are usually blank (see above).

# Actor input Schema

## `searchTerms` (type: `array`):

The keywords to measure. Each term is scraped as its own Google Trends query, so its 0-100 interest values are scaled to its OWN peak — switch on 'Compare terms against each other' if you need the values to be comparable BETWEEN terms. Leave empty and the actor runs the documented sample ("bitcoin", US, past 12 months) so it never returns nothing.

## `geos` (type: `array`):

One or more Google Trends location codes. Empty string "" = Worldwide. Use an ISO country code for a country (`US`, `GB`, `DE`, `IN`), or `COUNTRY-SUBDIVISION` for a state / province / region (`US-CA`, `US-NY`, `GB-ENG`, `CA-ON`). Every search term is scraped once per location, so 10 terms x 3 locations = 30 queries.

## `timeframe` (type: `string`):

How far back to measure. Google picks the data granularity from this: 'Past hour' and 'Past 4 hours' return one point per minute, 'Past day' every 8 minutes, 'Past 7 days' hourly, 'Past 1/3 months' daily, 'Past 12 months' and 'Past 5 years' weekly, and '2004 - present' monthly. Ignored when 'Custom date range' is filled in.

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

Overrides 'Timeframe' when set. Two ISO dates separated by a space, e.g. `2024-01-01 2024-12-31`. Google chooses the granularity from the span: under ~9 months returns daily points, longer returns weekly, several years returns monthly. Data starts at 2004-01-01.

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

Restrict the measurement to one Google Trends topic category, which strips out same-spelling searches from other subjects. 0 = All categories. Common IDs: 7 Finance, 12 Business & Industrial, 5 Computers & Electronics, 45 Health, 71 Food & Drink, 20 Games, 174 Autos & Vehicles, 958 Sports, 8 Arts & Entertainment, 67 Travel, 29 Jobs & Education.

## `searchProperty` (type: `string`):

Which Google surface to measure interest on. Web search is the default; the others measure the same term inside Google Images, Google News, YouTube search and Google Shopping, and their curves can differ sharply from web search.

## `includeInterestOverTime` (type: `boolean`):

Return one row per search term and date — the Google Trends line chart, flattened. This is the main output; switching it off leaves only the by-region rows.

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

Also return one row per search term and region — the Google Trends map, flattened, with each region's 0-100 value and its rank. Worldwide gives ~250 countries, a country gives its states/provinces, and a state gives its metro areas. Costs one extra request per query.

## `includeRegionsWithNoData` (type: `boolean`):

Google pads the region list with entries it flags as having no data. Measured 2026-09-02: a worldwide query returned 250 countries of which only 54 carried data, and City detail returned 200 cities of which only 10 did. Those blanks are dropped by default — not delivered and not charged. Switch this on if you are drawing a map and need every shape returned.

## `regionResolution` (type: `string`):

How finely to break down 'Interest by region'. 'Automatic' uses the level Google shows for your location (countries worldwide, states for a country, metros for a state). You can force a FINER level — Metro or City — but not a coarser one; if the level you pick is unavailable for that location the actor logs it and falls back to Automatic.

## `compareKeywords` (type: `boolean`):

Put the search terms into ONE Google Trends comparison (max 5 per group) so their values are scaled to a single shared peak and are directly comparable — 'brand A is 3x brand B'. Off by default, because each term measured on its own is scaled to its own peak, which is what you want for tracking a large keyword list.

## `language` (type: `string`):

The `hl` locale Google answers in. It changes the human-readable labels only — `formattedTime`, `timeframeLabel` and the region names in `regionName`. The numbers are identical in every language.

## `maxItems` (type: `integer`):

Stop after this many rows and end the run cleanly. A 12-month weekly series is 53 rows per term, and interest by region adds ~50 more for a US query or ~250 for a worldwide one. 0 = no limit.

## `maxConcurrency` (type: `integer`):

How many queries to run at once. Google Trends rate-limits hard: measured over 20 keywords, 1 at a time succeeded on 18/20 first attempts while 4 at a time succeeded on only 8/20. Raising this makes a run faster but pushes more queries into the retry path. Leave at 2 unless you are re-running a small list.

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

Apify Proxy is ON by default and should stay on. Google Trends rate-limits per IP, and this actor takes a fresh proxy session for every query and again for every retry — that rotation is the whole defence against the 429s. Any proxy group works; no residential requirement was measured.

## Actor input object example

```json
{
  "searchTerms": [
    "bitcoin"
  ],
  "geos": [
    "US"
  ],
  "timeframe": "today 12-m",
  "customTimeRange": "",
  "category": 0,
  "searchProperty": "",
  "includeInterestOverTime": true,
  "includeInterestByRegion": true,
  "includeRegionsWithNoData": false,
  "regionResolution": "auto",
  "compareKeywords": false,
  "language": "en-US",
  "maxItems": 200,
  "maxConcurrency": 2,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `interest` (type: `string`):

The dataset of scraped Google Trends interest rows.

# 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 = {
    "searchTerms": [
        "bitcoin"
    ],
    "geos": [
        "US"
    ],
    "timeframe": "today 12-m",
    "includeInterestOverTime": true,
    "includeInterestByRegion": true,
    "maxItems": 200
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/google-trends-interest-scraper").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 = {
    "searchTerms": ["bitcoin"],
    "geos": ["US"],
    "timeframe": "today 12-m",
    "includeInterestOverTime": True,
    "includeInterestByRegion": True,
    "maxItems": 200,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/google-trends-interest-scraper").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 '{
  "searchTerms": [
    "bitcoin"
  ],
  "geos": [
    "US"
  ],
  "timeframe": "today 12-m",
  "includeInterestOverTime": true,
  "includeInterestByRegion": true,
  "maxItems": 200
}' |
apify call scrapersdelight/google-trends-interest-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/kOXyMVnE7U4m4Z7Op/builds/ayxajPkRaOnAxLuzR/openapi.json
