# Bulk Keyword Search Volume, CPC & SEO Difficulty Checker (`s-r/keyword-metrics-bulk`) Actor

Bring your own keyword list and get monthly search volume, CPC, competition, SEO difficulty and paid difficulty for every one of them, in 50+ countries. Optionally add the recent monthly trend, the desktop/mobile split and search intent.

- **URL**: https://apify.com/s-r/keyword-metrics-bulk.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** SEO tools, Marketing
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 keyword analyzeds

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

## Bulk Keyword Search Volume, CPC & SEO Difficulty Checker

Paste a keyword list, get monthly search volume, cost per click, advertiser
competition, SEO difficulty and paid difficulty for every one of them, in 50+
countries. No subscription, no seat licence, no API key to obtain. Run it from
the Apify Store with one click, or call it from your own service via the Apify
API, and pay only for the rows you get back.

### What it is for

Most keyword tools answer the question "what else could I target?". This one
answers a different one: **"here are the keywords I already care about, tell me
what they are worth."** That is the job when you have a list and need numbers
against it, for example:

- a content calendar somebody already wrote, that needs volume before it gets
  approved
- a competitor's ranking keywords exported from somewhere else, that need
  difficulty scores to be triaged
- a Google Ads account's search terms report, that needs CPC and paid
  difficulty to decide what to bid on
- a product catalogue turned into search phrases, to see which categories
  actually have demand in a new country before you localise anything

If instead you want to *discover* keywords from one seed, use the
**Google Keywords Research** Actor, which expands a seed through autocomplete
first. This Actor deliberately does no expansion: what you put in is what you
get out, in the same order.

### What every row carries

| Field | What it means |
|---|---|
| `keyword` | The keyword, exactly as you supplied it |
| `volume` | Average monthly searches in the chosen country |
| `cpc` / `cpc_usd` | Cost per click, local currency and US dollars |
| `competition` | Advertiser competition, 0 to 1 |
| `seo_difficulty` | How hard the organic top ten is, 0-100 |
| `paid_difficulty` | How contested the paid results are, 0-100 |
| `updated_at` | When the source last refreshed this keyword |
| `found` | False when the source holds nothing for this keyword |

Turn on **Include monthly trend, device split and intent** and each row also
gets `monthly_volume`, `desktop_share`, `mobile_share`, `search_intent` and
`global_breakdown`.

### Two numbers that are easy to misread

**`cpc` is in local currency, `cpc_usd` is not.** A Dutch keyword at `cpc`
0.52 and `cpc_usd` 0.60 is the same click priced twice. If you are comparing
across countries, compare `cpc_usd` or you will conclude that the cheapest
market is whichever one has the weakest currency.

**`paid_difficulty` saturates at 100 quickly.** On commercial keywords it is
100 far more often than not, so it separates "commercial" from
"non-commercial" well and separates two commercial keywords from each other
badly. `competition` and `cpc_usd` do the finer work.

### What the source will and will not tell you

`found: false` means the source holds no record for that keyword. The row is
still returned, with every metric `null` rather than `0`, because a keyword
with genuinely zero volume and a keyword the source has never seen are
different facts and a `0` would hide the difference.

`monthly_volume` holds **four recent months**, not a rolling year. That is the
source's own window. It is enough to see a direction, not enough to establish
seasonality: for a yearly pattern you need a dedicated trends tool.

Volume figures are monthly aggregates and are refreshed on the source's own
schedule, which `updated_at` reports per keyword. A recent stamp does not mean
today's traffic.

If the source does not answer at all, the run still succeeds but writes **zero
rows** and sets `upstreamReachable: false` in the run's OUTPUT record. Nothing
is billed. That is deliberate: a dataset full of zeroes would look like a real
answer, and silently wrong numbers are worse than no numbers.

### Speed, and why the trend option is slower

Core metrics are fetched ten keywords per request, so 500 keywords is 50
requests and finishes in well under a minute. The trend option costs **one
request per keyword** instead of one per ten, because only the single-keyword
endpoint carries the monthly series and the device split. On a 500-keyword list
that is 500 requests rather than 50. Leave it off unless you need those fields.

### Input

```json
{
  "keywords": ["running shoes", "best running shoes", "trail running shoes"],
  "country": "us",
  "language": "en",
  "include_trend": false
}
```

`keywords` accepts a list or pasted text, one per line. Duplicates are removed
case-insensitively and the first spelling you used is the one returned.

### Free plan

On the Apify free plan a run returns at most 10 rows and stops there, and the
run's OUTPUT record says so. The Actor stops fetching once it has enough for
that limit rather than fetching your whole list and discarding most of it, so a
capped run is fast instead of slow and pointless. Any paid Apify plan removes
the cap.

### Countries

50+ countries by ISO code: ar, at, au, be, br, ca, ch, cl, cn, co, cz, de, dk,
eg, es, fi, fr, gb, gr, hk, hu, id, ie, il, in, it, jp, kr, mx, my, nl, no, nz,
pe, ph, pl, pt, ro, rs, ru, sa, se, sg, sk, th, tr, tw, ua, us, ve, vn, za. An
unrecognised code falls back to `us`.

# Actor input Schema

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

The keywords to look up. Paste one per line. Duplicates are removed and the order you give is the order you get back.

## `country` (type: `string`):

ISO 2-letter country code the volume should be measured in (us, nl, de, gb, fr and 45 more).

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

ISO 2-letter language code (en, nl, de, fr).

## `include_trend` (type: `boolean`):

Adds the monthly volume series, the desktop/mobile share and the search intent. This costs one extra request per keyword instead of one per ten, so a large list takes noticeably longer.

## `max_keywords` (type: `integer`):

Stop after this many keywords. Leave empty to process the whole list.

## `concurrency` (type: `integer`):

Parallel requests to the metrics source. Lower it if you see failed batches.

## Actor input object example

```json
{
  "keywords": [
    "coffee maker",
    "espresso machine"
  ],
  "country": "us",
  "language": "en",
  "include_trend": false,
  "concurrency": 8
}
```

# Actor output Schema

## `results` (type: `string`):

One item per keyword you supplied, in the order you supplied it.

## `output` (type: `string`):

OUTPUT record: how many keywords were asked for, how many came back, and whether the source answered at all.

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

Failures with a code and a redacted message. Absent when the run had none.

# 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": [
        "running shoes",
        "best running shoes",
        "trail running shoes"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/keyword-metrics-bulk").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": [
        "running shoes",
        "best running shoes",
        "trail running shoes",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("s-r/keyword-metrics-bulk").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": [
    "running shoes",
    "best running shoes",
    "trail running shoes"
  ]
}' |
apify call s-r/keyword-metrics-bulk --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/keyword-metrics-bulk"
        }
    }
}
```

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/LYoDgyoDz51akb9Au/builds/P8NttUfkPUzAO9gxF/openapi.json
