# Search interest over time

Canonical page: https://agentsky.dev/tools/dataforseo/keywords_google_trends_explore_live
JSON record: https://gateway.agentsky.dev/v1/endpoints/dataforseo/keywords_google_trends_explore_live

Search interest over time is a tool from DataForSEO, callable through the AgentSky API as dataforseo/keywords_google_trends_explore_live and billed per call (—).

Reports how search interest in a set of terms has moved over time, as a relative popularity index rather than absolute counts. Up to five terms can be compared in one call, returning a value at each interval in the chosen range with explicit markers where data is missing, plus averages across the period. A geographic breakdown shows which regions searched most for each term, and related topic and query lists reveal what people search alongside them. The trend can be drawn from web, news, video, image, or shopping search, which gives materially different pictures for the same term.

A time series of relative interest per term, with averages, and any regional, topic, or query blocks requested.

## Facts

- **Id:** `dataforseo/keywords_google_trends_explore_live`
- **Provider:** DataForSEO
- **Kind:** tool
- **Billed per:** call
- **Timeout:** 3 min
- **Max request:** 1 MB

## Call it

`POST https://gateway.agentsky.dev/v1/run` with `{"provider": "dataforseo", "endpoint": "keywords_google_trends_explore_live", "input": {…}}` and `Authorization: Bearer $AGENTSKY_API_KEY`.

### Input fields

| Field | Type | Required | Description |
|---|---|---|---|
| `keywords` | array | no | Keywords. Required if you don't specify category_code the maximum number of keywords. |
| `location_code` | integer | no | Search engine location code. Optional. if you don't use this field, you will recieve global results. |
| `language_code` | string | no | Search engine language code. Optional. default value: en. |
| `category_code` | integer | no | Google trends search category. Required if you don't specify keywords if you don't specify keywords, the value of this field must be greater than 0 if you specify keywords and don't specify this field, the 0 value will be applied by default and the search will be carried out across all available categories. |
| `location_name` | string | no | Which market to report on; omit for worldwide interest. |
| `type` | string | no | Which search property to draw the trend from. Allowed: web, news, youtube, images, froogle. Default: "web". |
| `time_range` | string | no | A preset window, ignored when explicit dates are given. |
| `date_from` | string | no | Start of the range, as `YYYY-MM-DD`. |
| `date_to` | string | no | End of the range, as `YYYY-MM-DD`. |
| `item_types` | array | no | Which blocks to return: the graph, the map, topics, or queries. |

## Example

### curl

```bash
# Set once per job; reuse this key and the identical body after a timeout.
: "${AGENTSKY_IDEMPOTENCY_KEY:?Set a unique, saved key for this job}"
curl --fail-with-body https://gateway.agentsky.dev/v1/run \
  -H "authorization: Bearer $AGENTSKY_API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: $AGENTSKY_IDEMPOTENCY_KEY" \
  -d '{"provider":"dataforseo","endpoint":"keywords_google_trends_explore_live","input":{"category_code":3,"keywords":["rugby","cricket"]}}'
# If status is READY/RUNNING, poll GET /v1/runs/{runId} with the same API key.
```

### JavaScript

```js
// Node.js 20+. Save one unique key per job; reuse it with the identical body.
const key = process.env.AGENTSKY_IDEMPOTENCY_KEY;
if (!key || !process.env.AGENTSKY_API_KEY) {
  throw new Error("Set AGENTSKY_API_KEY and a saved AGENTSKY_IDEMPOTENCY_KEY first");
}
const base = "https://gateway.agentsky.dev";
const headers = {
  authorization: `Bearer ${process.env.AGENTSKY_API_KEY}`,
  "content-type": "application/json",
  "Idempotency-Key": key,
};
const body = JSON.stringify({
  "provider": "dataforseo",
  "endpoint": "keywords_google_trends_explore_live",
  "input": {
    "category_code": 3,
    "keywords": [
      "rugby",
      "cricket"
    ]
  }
});
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function request(path, options = {}) {
  for (let attempt = 0; attempt < 3; attempt++) {
    let response;
    let data;
    try {
      response = await fetch(base + path, {
        ...options, headers, signal: AbortSignal.timeout(180_000),
      });
      const text = await response.text();
      try { data = JSON.parse(text); } catch { /* Proxy timeouts may return HTML. */ }
    } catch (error) {
      if (attempt === 2) throw error; // Resume with the same key and body.
      await sleep(2000);
      continue;
    }
    // Failed runs can have non-2xx HTTP status. Never regenerate them automatically.
    if (data?.runId && data?.status) return data;
    if (response.ok && data) throw new Error("Unexpected response: missing runId/status");
    if (attempt < 2 && (response.status >= 500 || response.status === 408 || response.status === 429)) {
      await sleep(2000);
      continue; // POST retries keep the original key and body.
    }
    throw new Error(`HTTP ${response.status}: ${JSON.stringify(data ?? "Non-JSON response; retry with the same key and body")}`);
  }
}

let run = await request("/v1/run", { method: "POST", body });
console.log("Save runId:", run.runId);
const deadline = Date.now() + 30 * 60_000;
while (["READY", "RUNNING"].includes(run.status)) {
  if (Date.now() >= deadline) {
    throw new Error(`Still pending: poll /v1/runs/${run.runId} later; do not create a new job`);
  }
  await sleep(5000);
  run = await request(`/v1/runs/${encodeURIComponent(run.runId)}`);
}
console.log("Status:", run.status, "Charge:", run.price?.amount);
if (run.status !== "COMPLETED") {
  throw new Error(`Run ${run.runId}: ${JSON.stringify(run.error ?? run.status)}`);
}
console.log(run.output); // Output shape depends on the endpoint.
// READY/RUNNING price is provisional. Use the terminal receipt for reconciliation.
```

## Pricing

— per call

Reported usage is billed under your account plan. Read the final customer charge from price.amount.value after the run finishes. A RUNNING response is not the final bill.

## Questions

### What does Search interest over time do?

Reports how search interest in a set of terms has moved over time, as a relative popularity index rather than absolute counts. Up to five terms can be compared in one call, returning a value at each interval in the chosen range with explicit markers where data is missing, plus averages across the period. A geographic breakdown shows which regions searched most for each term, and related topic and query lists reveal what people search alongside them. The trend can be drawn from web, news, video, image, or shopping search, which gives materially different pictures for the same term.

### What does Search interest over time take?

Nothing is required. Send an empty input object in a POST to /v1/run.

### What are the limits on Search interest over time?

The catalog lists an execution timeout of 3 min. A request can return READY or RUNNING before completion; poll GET /v1/runs/{runId} with the same API key until it reaches a terminal status. An HTTP timeout does not prove the generation stopped.

### How do I retry safely and check the final charge?

Save a unique Idempotency-Key for each job. After a network error or HTML timeout, resend the identical POST body with the same key and API key to recover the existing run, then poll its runId. A new key can start another billable generation. An HTTP error or timeout does not establish the run outcome or final charge. Pending runs can still settle; check the terminal price.amount and billing state, and retain the runId for reconciliation. Do not automatically create a new job after a terminal failure.
