# Keyword Difficulty Checker — Bulk Keyword Difficulty Scores (`steadyfetch/keyword-difficulty-scraper`) Actor

Keyword difficulty for your whole list — a 0–100 score per keyword with Google Ads search volume, CPC, competition and search intent on the same row; a keyword with no score is never charged. From $2.00 per 1,000 keyword results; one $0.19 fresh-lookup fee per run that buys new data.

- **URL**: https://apify.com/steadyfetch/keyword-difficulty-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** SEO tools, Agents, Developer tools
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 keyword results

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

## Keyword Difficulty Checker — Bulk Keyword Difficulty Scores

**Bulk keyword difficulty for your whole list — and never pay for a keyword with no score, or for one you already have.** Paste a keyword list and get a 0–100 keyword difficulty score for each one, with Google Ads monthly search volume, CPC, competition, search intent, the top-ten backlink averages and a 12-month trend on the same row.

**Using an AI agent?** Pin this actor in Apify's MCP server with one link: `https://mcp.apify.com?tools=steadyfetch/keyword-difficulty-scraper`

- **Actor id:** `steadyfetch/keyword-difficulty-scraper`
- **Input:** `{ "keywords": ["project management software"] }` — the one field you have to set.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `0.50` — the run stops when it reaches it.
- **How often it changes:** the source refreshes its keyword metrics once a month, so the same keyword asked again inside 30 days is the same score — and your account gets it back with nothing charged (`repeat: true`).
- **Run it on a schedule:** save your input as a Task and put it on an Apify Schedule once a month. Each run looks up and charges only the keywords your account has not had in the last 30 days.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

**See a real run before you spend anything:** [sample dataset](https://api.apify.com/v2/datasets/zuWzAad6KxVXftuAc/items?clean=true\&format=json) — five standing-desk keywords for the United States from one verified run, unedited: four scored rows, charged; one long-tail phrase with no score on record, returned uncharged; and the run's own summary row last.

**What the score means.** Keyword difficulty is how hard it is to rank in Google's top 10 organic results for a keyword, on a logarithmic scale from 0 (easy) to 100 (extremely hard). It is computed from the link profiles of the pages ranking in the current top 10 for that keyword, one country and language at a time — so it is a model, not a Google number, and this README says exactly what it is built from. Search volume, CPC, competition and the bid range on the same row are Google Ads figures.

**Just want to see it work?** Click **Start** with nothing set and the run scores two everyday keywords ("project management software", "keyword research tool") for the United States, charged like any run — two keyword rows, plus the fresh-lookup fee only if those two keywords are not already in our 30-day cache. Pick a country or a limit but paste no keywords and that same sample runs **under your settings**, with one uncharged note row naming them. Paste your own list for your own run.

*Unofficial. This actor is not affiliated with, endorsed by, or sponsored by Google. "Google" and "Google Ads" are trademarks of Google LLC, used here only to describe where the figures come from.*

**From $2.00/1,000 keyword results** — all-inclusive pay per event, charged only on delivery. **A run that buys fresh keyword data also pays one fresh-lookup fee of $0.19** — once per run, whatever the size of the list. Runs answered entirely from our 30-day cache, and runs whose fresh lookup returns no score for any keyword, pay no fresh-lookup fee.

| Apify plan | per keyword row | 1,000 keywords |
|---|---|---|
| Apify free plan | $0.008 | $8.00 |
| Bronze | $0.006 | $6.00 |
| Silver | $0.004 | $4.00 |
| Gold and above | $0.002 | $2.00 |

***

### Output

One row per keyword you sent — delivered with a score, or an uncharged row saying why not — plus one summary row at the end.

| field | notes |
|---|---|
| `keyword` | your own spelling, echoed back; `providerKeyword` is the lowercased form the source matched |
| `keywordDifficulty` | **the score, 0–100** — 0 is easy, 100 is extremely hard. `null` only on an uncharged row |
| `avgMonthlySearches` | Google Ads average monthly searches for the market |
| `cpcUsd` | average cost per click, in USD |
| `competition` · `competitionIndex` | `LOW` / `MEDIUM` / `HIGH` paid-search competition, and the 0–100 index behind it |
| `lowTopOfPageBidUsd` · `highTopOfPageBidUsd` | the top-of-page bid range |
| `searchIntent` | `informational` / `navigational` / `commercial` / `transactional` |
| `avgBacklinksTop10` · `avgReferringDomainsTop10` | the average backlinks and referring domains of the pages in the current top 10 — what the score is built from |
| `monthlySearchVolumes[]` | the latest 12 months of `{year, month, searches}`, **oldest month first** |
| `searchVolumeChangePct` | `{monthly, quarterly, yearly}` change in search volume, in percent |
| `trendDirection` | `rising` / `flat` / `falling`, derived from the 12-month series |
| `dataAsOf` | the last month in that series, e.g. `2026-08` |
| `emptyFields` | every promised field this row came back without, each with its reason. `{}` on a complete row |
| `country` · `language` | the market this score is for |
| `servedFromCache` | whether this row came from our 30-day cache |
| `repeat` · `firstSeenAt` · `firstSeenRunId` | `true` when your account already had this keyword: the same figures handed back from the run named here, and nothing charged for them |
| `charged` · `missReason` · `statusReason` | the reconciliation trio |

And one delivered row in full, from the probe run on 2026-10-01 (United States, English). Each keyword with a score is one `keyword-result` charge; a keyword the source holds no score for is never charged.

```json
{
  "schemaVersion": 1,
  "keyword": "keyword difficulty",
  "providerKeyword": "keyword difficulty",
  "keywordDifficulty": 29,
  "avgMonthlySearches": 320,
  "cpcUsd": 10.57,
  "competition": "LOW",
  "competitionIndex": 17,
  "lowTopOfPageBidUsd": 3.28,
  "highTopOfPageBidUsd": 14.82,
  "searchIntent": "informational",
  "avgBacklinksTop10": 452.9,
  "avgReferringDomainsTop10": 163.1,
  "monthlySearchVolumes": [
    {"year":2025,"month":9,"searches":170},
    {"year":2025,"month":10,"searches":90},
    {"year":2025,"month":11,"searches":70},
    {"year":2025,"month":12,"searches":110},
    {"year":2026,"month":1,"searches":90},
    {"year":2026,"month":2,"searches":260},
    {"year":2026,"month":3,"searches":1300},
    {"year":2026,"month":4,"searches":480},
    {"year":2026,"month":5,"searches":720},
    {"year":2026,"month":6,"searches":390},
    {"year":2026,"month":7,"searches":480},
    {"year":2026,"month":8,"searches":170}
  ],
  "searchVolumeChangePct": {"monthly":-65,"quarterly":-56,"yearly":-47},
  "trendDirection": "rising",
  "dataAsOf": "2026-08",
  "emptyFields": {},
  "country": "US",
  "language": "en",
  "servedFromCache": false,
  "charged": true,
  "repeat": false,
  "missReason": null,
  "retryable": false,
  "statusReason": "Keyword difficulty with Google Ads volume and CPC."
}
```

On the same run `running shoes` scored **43** and `best running shoes for flat feet women` scored **9** — the head term is contested, the long tail is wide open. That is the comparison a difficulty score is for.

### When a promised field comes back empty

The score is the product, and a row without one is never charged. The other columns are Google's own figures, and Google does not publish every figure for every keyword — some come back with no average CPC, no bid range, no competition level or a short history. We never invent the number, and we never quietly leave the column blank either: **every promised field that arrives empty says why, on that row**, in `emptyFields` and in `statusReason`.

| reason | what it means |
|---|---|
| `no-search-volume-reported` | Google reports no search volume for this keyword — the score still ships |
| `no-historical-avg-cpc` | a top-of-page bid range but no average CPC — use the bid range as the cost signal |
| `no-advertiser-cost-data` | no average CPC **and** no bid range |
| `no-bid-range-reported` | the CPC arrived, the bid range did not |
| `no-competition-data` | no competition level or index |
| `no-search-intent-reported` | no search intent is classified for this keyword |
| `no-top-ten-backlink-data` | no backlink averages for the current top ten are on record |
| `fewer-than-six-months-of-data` | too short a series to call a trend — the months themselves still ship |
| `no-monthly-series` | no month-by-month series, so there is no `dataAsOf` either |

***

### Agent / API paste-block

```
Actor:      steadyfetch/keyword-difficulty-scraper
Required:   keywords        (array of keywords; leave it empty for a two-keyword sample, charged like
                             any run — settings you do change scope that sample)
Optional:   keywordsText    (string — paste a list, newline or comma separated)
            country         (ISO-2, ISO-3, the name in English or the country's own language, the
                             Google location code, a locale tag, or the first country in a comma
                             list — US, USA, GB, Deutschland, 2840, en-GB; default US)
            language        (code, BCP-47 tag or English name — leave it out for the country's main
                             language; it must be one the country is reported in)
            maxItems        (integer, default 1000 — up to 100,000 for the whole run)
            maxRunSeconds   (integer, default 900 — 30 s to 1 h accepted, a run never passes 30 min; clean stop, never a timeout kill)
Charges:    keyword-result  once per keyword row delivered with a score
            keyword-lookup  $0.19 once per run that buys fresh data and delivers at least one row
                            (cache-only and no-score runs pay none)
Run option: Maximum cost per run — at least $0.25 so a fresh lookup fits
```

Agents and MCP callers: **omit an optional field, or send it as `null` — both mean "use the default".** Only `keywords` needs a real value. Send the fields themselves as the run input — `{"keywords": [...]}`, not `{"input": {"keywords": [...]}}`; a wrapped input is still read, and one uncharged note row says so.

***

### How the trend direction is worked out

The last three months of the 12-month series against the first three of the same series. 20% or more above → `rising`. 20% or more below → `falling`. Anything between → `flat`. Fewer than six months of data → `null`, never a guess. The raw series ships with every row, so you can apply your own rule instead — and `searchVolumeChangePct` carries the source's own month, quarter and year changes beside it.

***

### The 30-day cache

The source refreshes these figures **monthly**, so a keyword looked up twice in the same month returns the same score. We cache every lookup for 30 days and serve repeats instantly. Rows served that way are marked `servedFromCache: true`. That cache is shared across everyone who uses this actor, so it saves the lookup, not the row: a keyword someone else looked up is still your first one, charged at the same per-keyword price. A run answered entirely from the cache also pays **no fresh-lookup fee**.

**The cache is per keyword AND market.** Difficulty is scored one country and language at a time, so `seo tools` in the United States and `seo tools` in Germany are two different answers and two different cache entries. The run's summary row says how many of its rows the cache answered. A keyword the source holds no score for is cached as that answer too, so asking again inside 30 days buys nothing.

### You are never charged twice for the same keyword

Ask for a keyword you already got from us inside those 30 days and the same figures are **handed straight back, with nothing charged for them**. No lookup is bought and no `keyword-result` event is charged — the check happens before either. Those rows carry `repeat: true`, `charged: false`, `firstSeenAt` and `firstSeenRunId`. Past 30 days the source has refreshed its figures, so the same keyword is **new data and a new charge**.

This memory is yours: it lives in **your own account**, in a key-value store called **`kw-difficulty-account`** on your Storage tab. Delete it to start over. If it cannot be read on a given run, the run still delivers — it charges as it always did and the status line says the check was unavailable. A run started with a **scoped API token** in restricted-access mode needs key-value store **Read, Write and Create** permission (or Actor runs set to **Full access**) under Settings → API & Integrations; without it the run says so on its status line and in one uncharged row.

### Run it on a schedule — monthly is the cadence that pays

The source refreshes these figures **monthly**, so a monthly schedule is the one that brings back numbers you do not already have. Save your input as a **Task**, put the Task on an Apify **Schedule**, and add an integration or a webhook on *run succeeded* — a Google Sheet, Slack, n8n, Make, or your own endpoint. Growing a tracked list stays cheap: only the keywords your account has not had in the last 30 days are looked up and charged. Chart by `dataAsOf` to see whether a run brought a new month.

***

### What you are never charged for

| situation | `missReason` | run status |
|---|---|---|
| The source holds no difficulty score for that exact phrase in that market | `NO_DATA` | SUCCEEDED — `keywordDifficulty: null`, and the row names the head term to try |
| A keyword over 80 characters, over 10 words, built from emoji, or carrying a symbol a Google keyword cannot hold | `INVALID_KEYWORD` | SUCCEEDED — the row names the rule |
| The source rate-limited us | `THROTTLED` | SUCCEEDED — re-runnable, and the row says so |
| The source was unavailable this run | `PROVIDER_UNAVAILABLE` | SUCCEEDED — re-runnable |
| We could not reach the source at all | `PROVIDER_ACCOUNT` | SUCCEEDED — this one is on us, never on you |
| A language the country is not reported in (English for Germany, say) | `USER_INPUT` | SUCCEEDED — one row per keyword, uncharged, naming the languages that work there |
| A country we could not place, a whole-region ask (worldwide, EU), a city or region code, or a pasted block in the Country box | `USER_INPUT` | SUCCEEDED — nothing is looked up; one row per keyword, uncharged, naming the value and the values that work |
| A country the source does not score (China, Russia, Kuwait, Qatar…) | `USER_INPUT` | SUCCEEDED — uncharged, names the country |
| Your own `maxItems` / `maxRunSeconds` / cost cap | `STOPPED_AT_LIMIT` | SUCCEEDED — summary row + `resumeCursor` |
| A keyword your account already had in the last 30 days | — | SUCCEEDED — `repeat: true`, the same figures handed back, nothing charged |
| A run answered entirely from the 30-day cache | — | no fresh-lookup fee |
| A fresh lookup that returned no score for any keyword | `NO_DATA` on every row | no fresh-lookup fee, no keyword charges |
| A cost cap too low to buy one fresh lookup plus one keyword | `STOPPED_AT_LIMIT` | SUCCEEDED — cached keywords still delivered, summary row names the fix |
| A keyword needing fresh data past either of the Apify free plan's daily lookup allowances | `FREE_PLAN_FRESH_CAP` | SUCCEEDED — cached keywords still delivered and charged, the rest named on uncharged rows and in `resumeCursor` |

We never guess. A rate limit is reported as a rate limit, not as "this keyword has no score" — a temporary problem is never dressed up as a permanent verdict.

**This actor may fail when the platform changes things — failed items are never charged.**

***

### Limits are hard limits

`maxItems` and `maxRunSeconds` stop the run exactly where you set them. The run still finishes **successfully**, the summary row names which limit bound, and `resumeCursor` lists the keywords that were not reached — so you continue from there instead of paying to look up what you already have. A time limit ends the collecting, never the delivering: a score already in hand is still delivered.

Each also has a range of its own: 100,000 rows for the whole run, and a run window between 30 seconds and one hour — though a run never works past 30 minutes, this actor's own ceiling. Ask outside one of them and the run **still starts**: it uses the nearest end of the range and writes one uncharged row naming what you asked for and what bound it.

A five-keyword run costs five keyword rows plus one $0.19 fresh-lookup fee if those keywords were not already in the cache; a run where nothing has a score costs nothing. The run's **Maximum cost per run** (a run option) must cover one fresh lookup plus one keyword row — $0.25 is the minimum. Below it, keywords already in the cache are still delivered, no fresh lookup is bought, and the summary row says exactly what to raise.

**And the cap belongs to the run, not to the attempt.** If a run is moved to another server mid-way — or you resurrect it from the console — it continues from where it stopped: no row you already have is delivered again, no keyword is looked up twice, and no keyword is charged twice.

Fresh lookups can be paused on our side for maintenance. Then a run still delivers the keywords already in the 30-day cache, charged as usual, and ships one uncharged row for the rest.

On the Apify free plan this actor buys at most 1 fresh lookup per account per day: keywords already in the 30-day cache are still delivered and charged as usual, and keywords that need fresh data come back the next day, or on any paid Apify plan.

On that plan one keyword list is also looked up fresh for at most 2 accounts a day, whatever market each of them asks for, so the same list run by a crowd of Apify free plan accounts does not buy the same fresh data over and over: past those two, keywords already in the 30-day cache are still delivered and charged as usual, the rest ship as uncharged rows saying so, and they come back the next day or on any paid Apify plan. Paid plans are not affected by either allowance.

***

### FAQ

**How much does 1,000 keywords cost?**
From $2.00 on the Gold tier — $0.008 per keyword on the Apify free plan, falling with your Apify plan. That is 1,000 rows of the `keyword-result` event plus one $0.19 `keyword-lookup` fee if the run bought fresh data. Keywords with no score are not rows you pay for, and the volume, CPC, intent and backlink columns come on the same row at no extra charge.

**What is the fresh-lookup fee, and when is it waived?**
Our licensed source bills every fresh lookup as a request, however many keywords are in it. A run pays $0.19 once when it buys fresh data and delivers at least one keyword row. It is waived when the whole run is answered from our 30-day cache, and when the fresh lookup returns no score at all. It is never charged per keyword or per re-run of a resumed run.

**Is the difficulty score a Google number?**
No — Google does not publish one. The score is computed by the source from the backlink strength of the domains and pages ranking in the current top 10 for the keyword, in that country, on a logarithmic 0–100 scale. The backlink averages it is built from ship on the row (`avgBacklinksTop10`, `avgReferringDomainsTop10`), so you can see why a keyword scored the way it did. Search volume, CPC, competition and the bid range are Google Ads figures.

**Why did a keyword come back with no score?**
The source scores the keywords it tracks in each market's search results. A phrase it does not track comes back as its own uncharged row with `missReason: NO_DATA` and the head term to try — drop the city or the qualifier and score the broader phrase.

**Do duplicates cost twice?**
No. `SEO Tools`, `seo tools` and `  seo   tools  ` are one keyword, charged once, echoed back with the first spelling you used.

**Which countries and languages work?**
The **94 markets** the source scores — the United States, the United Kingdom, Canada, Australia, India, Germany, France, Spain, Italy, Brazil, Mexico, Japan, South Korea, the Gulf, most of Europe, Latin America and South-East Asia, and Hong Kong and Taiwan. Countries take a two-letter code, a three-letter code, the name in English **or in the country's own language**, or the Google location code: `US`, `USA`, `GB`, `UK`, `United Kingdom`, `Deutschland`, `España`, `日本`, `مصر`, `UAE`, `2840`. Case, spaces, hyphens, underscores and accents do not matter, and a locale tag like `en-GB` is read for its market.

Each market is reported in **its own languages**: leave Language empty and the country's main language is used (English for the US, German for Germany, Dutch for Belgium), or pick another one the country is reported in (Spanish for the US, French for Canada, French or Italian for Switzerland). A language the country is not reported in is refused, uncharged, and the row names the ones that work there — the run never swaps in a different language behind your back.

A country the source does not score — China, Russia, Kuwait, Qatar and the other markets outside the 94 — is refused by name rather than read as a typo. A region-wide ask like `worldwide` or `EU` is refused because difficulty is scored one country at a time. Any other value we cannot place — a typo, a made-up code, a Google location code for a city or region — **is refused before anything is looked up**: nothing is charged, and every keyword gets an uncharged row naming the value you sent and the values that work. We never guess a market for you.

**Does `{}` cost anything?**
Two keyword rows at your plan's price — under two cents — plus the fresh-lookup fee only if the two sample keywords are not already in our cache. With no keywords it runs the default sample — two everyday keywords for the United States — charged like any run, so you see the exact output shape on real data. The status line says it was the sample and how to run your own list.

**I want search volume only, or keyword ideas.**
For Google Ads search volume and CPC on a list, our [Keyword Search Volume Scraper](https://apify.com/steadyfetch/keyword-search-volume-scraper) is the volume-first sibling with its own 230-market table. This actor is the one that scores difficulty.

***

### Steadyfetch trends & keyword suite

One actor per surface, one job each, the same contract everywhere: **all-inclusive pay per event, charged only on delivery**.

| What you want | Actor |
|---|---|
| Keyword difficulty for a keyword list | **this actor** |
| Monthly search volume and CPC for a keyword list | [Keyword Search Volume Scraper](https://apify.com/steadyfetch/keyword-search-volume-scraper) |
| Interest over time, related queries, regions | [Google Trends Scraper](https://apify.com/steadyfetch/google-trends-scraper) |
| Rising and Breakout queries | [Trending Keywords Scraper](https://apify.com/steadyfetch/breakout-keywords-scraper) |
| Autocomplete suggestions across 5 engines | [Google Autocomplete Scraper](https://apify.com/steadyfetch/google-keyword-suggest-scraper) |
| What is trending right now, by country | [Google Trends Trending Now Scraper](https://apify.com/steadyfetch/google-trends-now-scraper) |

***

### Feedback & support

Found an issue? Open it on the **Issues tab** — we usually reply within a couple of hours, always within a day.

# Changelog

This Actor's version history is a separate document: https://apify.com/steadyfetch/keyword-difficulty-scraper/changelog.md

# Actor input Schema

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

The keywords to check keyword difficulty for, e.g. \["project management software", "crm for startups"] — any length, and one run delivers up to maxItems rows (1,000 by default, up to 100,000) within maxRunSeconds (900 by default, 30 minutes at most); a keyword with no score is never charged, so send the whole list. Keywords a run does not reach are not charged and are listed in resumeCursor; one your account got in the last 30 days comes back uncharged but counts toward maxItems. Leave it empty in the console and the run is a two-keyword sample, charged like any run. Settings you do change scope that sample: it runs in your market, under your limits, and one uncharged row names them.

## `keywordsText` (type: `string`):

Paste a whole keyword list here instead — one keyword per line, or comma-separated, e.g. "crm for startups, project management software". Merged with the list above; duplicates are charged once.

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

Country the keyword difficulty is measured in — pick one, or type a code or its name in English or its own language: US, USA, GB, United Kingdom, Deutschland, a locale tag like en-GB, the first country in a list like "Dubai, United Arab Emirates", and the Google location code (2840) all work. 94 markets are carried, one per run, so there is no worldwide, EU or region-wide score. A value we cannot place — a typo, a city or region code, a market the source has no data for, a whole-region ask or a pasted block — is refused before any lookup, uncharged, each row naming the values that work; no market is guessed for you.

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

Language of the searches as a code, a tag or the English name, e.g. "en", "en-US" or "German" — leave it empty to use the country's main language (English for the US, German for Germany). Each market is reported in its own languages only (Canada in English and French, Switzerland in German, French and Italian); a language the country is not reported in is refused, uncharged, and the row names the ones that work.

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

Max keyword rows for the whole run, a whole number up to 100,000, e.g. 500 — a hard cap on charged rows. Ask for more and the run continues at 100,000, with one uncharged row saying so.

## `maxRunSeconds` (type: `integer`):

Max run seconds, a whole number of seconds, e.g. 900 — the run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout. Anything from 30 seconds to an hour is accepted, but a run never works past 30 minutes, this actor's own ceiling, so a value above 1800 ends the run at 30 minutes; ask outside that range and the run continues at the nearest end of it, with one uncharged row saying so. The run's own Timeout (Run options, or timeout on an API call) is a separate limit: when it ends the run first, the status line says to raise that Timeout instead.

## Actor input object example

```json
{
  "keywords": [],
  "country": "US",
  "maxItems": 1000,
  "maxRunSeconds": 900
}
```

# Actor output Schema

## `keywords` (type: `string`):

One row per keyword: the 0–100 keyword difficulty score (how hard it is to rank in the top 10, computed from the backlink strength of the pages ranking there), with Google Ads average monthly searches, CPC, competition, the top-of-page bid range, search intent, the top-ten backlink averages and the 12-month series. Only rows with charged = true were billed — one keyword-result each — and only a row that carries a score is charged: a keyword the source holds no score for ships with keywordDifficulty null, charged = false and a missReason. A Google figure the source does not publish for a keyword is never invented — the row leaves it null and names the reason in `emptyFields`. A row with repeat = true is a keyword this account already had inside the last 30 days: the same figures handed back from the run named in firstSeenRunId, with charged = false. One keyword-lookup fee is charged per run that bought fresh data and delivered at least one row; the summary row names it.

## `trend` (type: `string`):

The same rows narrowed to the score, the month-by-month search volumes behind each keyword's trend direction (oldest month first) and the source's own month, quarter and year changes.

## `summary` (type: `string`):

Rows delivered, keywords with no score, keywords the source would not accept, cache hit rate, and what stopped the run.

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

Present only when a lookup was rate-limited or the source was unavailable: the keyword and the reason. These are re-runnable, not permanent.

# 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": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/keyword-difficulty-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 = { "keywords": [] }

# Run the Actor and wait for it to finish
run = client.actor("steadyfetch/keyword-difficulty-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 '{
  "keywords": []
}' |
apify call steadyfetch/keyword-difficulty-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyfetch/keyword-difficulty-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/rkU6xyQzfBoWVvnbQ/builds/kB9HddbldlByZlAcF/openapi.json
