Keyword Difficulty Checker — Bulk Keyword Difficulty Scores
Pricing
from $2.00 / 1,000 keyword results
Keyword Difficulty Checker — Bulk Keyword Difficulty Scores
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.
Pricing
from $2.00 / 1,000 keyword results
Rating
0.0
(0)
Developer
Steadyfetch Team
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
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
maxTotalChargeUsdon 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 — 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.
{"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-scraperRequired: keywords (array of keywords; leave it empty for a two-keyword sample, charged likeany 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, theGoogle location code, a locale tag, or the first country in a commalist — US, USA, GB, Deutschland, 2840, en-GB; default US)language (code, BCP-47 tag or English name — leave it out for the country's mainlanguage; 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 scorekeyword-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 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 |
| Interest over time, related queries, regions | Google Trends Scraper |
| Rising and Breakout queries | Trending Keywords Scraper |
| Autocomplete suggestions across 5 engines | Google Autocomplete Scraper |
| What is trending right now, by country | Google Trends Trending 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.