# Competitor Keywords Scraper — Ranked Keywords of Any Domain (`steadyfetch/competitor-keywords-scraper`) Actor

The keywords any domain or page ranks for on Google — position, ranking URL, search volume, CPC, keyword difficulty, search intent and estimated traffic for each, in 94 countries. From $1.50 per 1,000 keywords plus a $0.02 lookup per domain; a domain that ranks for nothing is never charged.

- **URL**: https://apify.com/steadyfetch/competitor-keywords-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 $1.50 / 1,000 ranked keywords

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

## Competitor Keywords Scraper — Ranked Keywords of Any Domain

**Competitor Keywords Scraper: the keywords any website ranks for on Google, with the numbers to act on
them.** Give it a competitor's domain, or one page, and get back the keywords it ranks for in Google's
top 100, one row each: its position, the page that ranks, monthly search volume, CPC, competition,
keyword difficulty (0–100), search intent and the traffic it is estimated to bring — in 94 countries.
A domain with no ranked keywords is never charged.

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

- **Actor id:** `steadyfetch/competitor-keywords-scraper`
- **Input:** `{ "domains": ["ahrefs.com"] }` — the one field you have to set. Add `"maxResultsPerDomain": 1000` for more keywords.
- **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, and never buys keywords it cannot sell you.
- **How often it changes:** the source re-checks the Google results behind each ranking about every 75 days, and each row's `lastUpdated` says when — a monthly schedule catches most moves.
- **Run it on a schedule:** save your input as a Task and run it monthly on an Apify Schedule; from the second run on, each row says where the keyword ranked last time (`previousPosition`, `positionChange`, `isNew`).

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/eEE7AZUEs8NlkZ8VJ/items?clean=true\&format=json) — ten keywords backlinko.com ranks for in the United States from one verified run, unedited, with the domain's overview row and the run's own summary row last: ten keywords delivered and charged, plus one lookup.

**Just want to see it work?** Click **Start** with nothing set and the run gets ten ranked keywords for
the example domain ahrefs.com in the United States, charged like any run. Pick a country or a limit but
set no domains and that same sample runs under your settings, with one uncharged note row; a limit above
ten is capped at ten. Put your own domains in **Domains or pages** for your own run.

*Unofficial. This actor is not affiliated with, endorsed by, or sponsored by Google. "Google" is a
trademark of Google LLC, used here only to describe where search data comes from.*

### What you get

One row per keyword the domain ranks for:

```json
{
  "rowType": "keyword",
  "domain": "ahrefs.com",
  "targetType": "domain",
  "keyword": "backlinks checker",
  "position": 1,
  "absolutePosition": 1,
  "url": "https://ahrefs.com/backlink-checker",
  "searchVolume": 90500,
  "cpc": 7.17,
  "competition": 0,
  "competitionLevel": "LOW",
  "keywordDifficulty": 82,
  "searchIntent": "informational",
  "estimatedTraffic": 27512,
  "lastUpdated": "2026-07-30T04:18:28.000Z",
  "previousCheckAt": "2026-05-15T03:34:38.000Z",
  "serpTrend": "same",
  "country": "US",
  "language": "en",
  "previousPosition": null,
  "positionChange": null,
  "isNew": null,
  "previousRunAt": null,
  "charged": true
}
```

| Field | What it is |
|---|---|
| `rowType` | `keyword` for a ranked keyword; the overview and summary rows carry their own. |
| `domain`, `targetType` | The domain or page you asked about, as it was read, and whether it was read as a whole `domain` or one `page`. |
| `keyword` | A keyword it ranks for. |
| `position` | Its organic position on Google, counting organic results only (1 = the top organic result). |
| `absolutePosition` | Its place counting every element on the results page, ads and boxes included. |
| `url` | The page of that site that ranks for the keyword. |
| `searchVolume` | Average monthly searches over the last 12 months. |
| `cpc`, `competition`, `competitionLevel` | Cost per click in USD, advertiser competition from 0 to 1, and LOW / MEDIUM / HIGH. |
| `keywordDifficulty` | 0–100: how hard it is to rank in the top 10 organic results. |
| `searchIntent` | informational, navigational, commercial or transactional. |
| `estimatedTraffic` | Monthly visits this position is estimated to bring — search volume times the click share of the position. An estimate, never a measured count. |
| `lastUpdated`, `previousCheckAt`, `serpTrend` | When the source last checked this results page, when it checked it before that, and how the position moved between those two checks: `up`, `down`, `same` or `new`. |
| `country`, `language` | The market the keyword was read in. |
| `previousPosition`, `positionChange`, `isNew`, `previousRunAt` | Where it ranked in your last run of the same domain and market, how many places it moved (a positive number means it climbed), whether it is new since that run, and when that run was. Empty (`null`) on your first run. |
| `charged` | Whether this row was billed. |

A figure the source does not publish comes back `null` — never invented.

**One overview row per domain**, uncharged: how many keywords the domain ranks for in total (often
far more than you asked for), how they spread over positions 1, 2–3, 4–10, 11–20 and on down to 100,
the estimated monthly traffic they bring, and how many of its rankings are new, up or down since the
source's check before. With a position or volume filter set, those totals count only the keywords
that pass it (`totalsMatchYourFilters: true`). **One summary row last**, uncharged: the run's receipt —
what was delivered, what was charged, and what stopped the run. Rows with a `missReason` are uncharged
notes saying why something was not delivered.

### Settings

| Setting | Field | Default |
|---|---|---|
| Domains or pages | `domains` | — up to 100. A bare domain or a homepage link reads the whole domain; a link with a path reads that one page |
| Country | `country` | `US` — 94 countries; a country the source does not cover is refused with an uncharged row, never swapped for another |
| Language | `language` | the country's main language |
| Max keywords per domain | `maxResultsPerDomain` | 100, up to 5,000 |
| Sort keywords by | `sortBy` | `traffic` (estimated traffic, biggest first); also `volume` and `position` |
| Ranking at or above position | `maxPosition` | none — set 1–100, e.g. `10` for page one only |
| Minimum search volume | `minSearchVolume` | none |

The filters run at the source, so a keyword outside them is never delivered or charged. The sort
decides which keywords a limit keeps: `traffic` gives the keywords that bring the domain the most
visits, `volume` the biggest searches it shows up for, `position` its best rankings first. Organic
Google results only.

### Pricing

Pay per event, all-inclusive — no platform-usage charge on top, and no start fee.

| Event | Apify free plan | Bronze | Silver | Gold and above |
|---|---|---|---|---|
| Ranked keyword (per keyword delivered) | $0.003 | $0.0025 | $0.002 | $0.0015 |
| Domain lookup (once per block of up to 1,000 keywords from one domain) | $0.02 | $0.02 | $0.02 | $0.02 |

100 keywords for one domain is one lookup: $0.17 on Gold, $0.32 on the Apify free plan. 1,000 keywords
is still one lookup; 5,000 is five. Ten domains at 100 keywords each are ten lookups.

**Never charged:** a domain or page with no ranked keywords (no lookup fee either), a keyword your
filters leave out, the overview and summary rows, and a request the source refuses or cannot answer.

### How often this data changes

Rankings move every day, but the source re-checks the Google results behind each ranking on its own
cycle: about every 75 days in our live reads (the middle case; from 74 to 218 days). In one read of a
large domain on 2026-10-03, its top 20 keywords had last been checked between 2026-06-17 and 2026-09-02,
and their search volumes had been refreshed in mid-September. Each row's `lastUpdated` says when its
result was checked, and `serpTrend` says how it moved since the check before — so even your first run
shows which rankings are climbing.

So the useful pattern is the same domains once a month — a monthly schedule catches most moves. A run
more often mostly returns the same positions, and every run reads the source anew and charges for what it delivers. Your own
Apify account remembers the positions each domain had, per country and language, and the next run of
the same domain fills `previousPosition`, `positionChange` and `isNew` from that memory: a monthly rank
tracker for your competitors, and for your own site. Keep the limit, sort and filters the same from run
to run — `isNew` compares against what your last run delivered, so a keyword that was only outside last
time's limit would read as new. When the sort or a filter changed, `isNew` stays empty;
`previousPosition` still comes from your last run.

**Set the schedule:** save your input as a Task (Actor page → Save as a new task), then add that
task to an Apify Schedule (Console → Schedules → Create) with a cron such as `0 7 1 * *` for the first of
every month.

### Limits are hard limits

`maxResultsPerDomain` and your maximum total charge stop the run exactly where they bind; the run still
finishes successfully and the last row says what was delivered and what stopped it. Keywords a request
already returned are always delivered.

On the Apify free plan, one domain lookup per account in any 24 hours is included — one domain, up to
1,000 of its keywords; past it the run stops, and that domain's uncharged overview row says when it
resets. Paid Apify plans carry no daily allowance.

### Steadyfetch keyword tools

| You want | Actor |
|---|---|
| The keywords a competitor's domain ranks for, with positions | **this actor** |
| New keyword ideas from a seed, with volume, CPC and difficulty | [Keyword ideas](https://apify.com/steadyfetch/keyword-ideas-scraper) |
| A 0–100 difficulty score for a keyword list you already have | [Keyword difficulty](https://apify.com/steadyfetch/keyword-difficulty-scraper) |
| Monthly search volume and CPC for a keyword list | [Keyword search volume & CPC](https://apify.com/steadyfetch/keyword-search-volume-scraper) |
| Autocomplete suggestions across 5 engines | [Autocomplete keywords](https://apify.com/steadyfetch/google-keyword-suggest-scraper) |
| Rising and Breakout queries with the growth number | [Breakout keywords](https://apify.com/steadyfetch/breakout-keywords-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/competitor-keywords-scraper/changelog.md

# Actor input Schema

## `domains` (type: `array`):

The domains or pages to get ranked keywords for, e.g. \["ahrefs.com"] — up to 100 per run, one per line. A bare domain or homepage (ahrefs.com, https://www.ahrefs.com/) reads the whole domain; a subdomain (blog.example.com) reads that subdomain; an address with a path (https://ahrefs.com/blog/) reads that one page. Entries that are not a domain are refused on their own uncharged row and never sent.

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

The country whose Google rankings to read — pick one, or type a code or name (US, USA, United Kingdom, en-GB). 94 countries are covered; one country per run.

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

The search language — a code or name (en, es, German). Leave empty for the country's main language; each country offers its own languages (the United States: English or Spanish).

## `maxResultsPerDomain` (type: `integer`):

How many ranked keywords to deliver per domain, at most, a whole number, e.g. 500 — 100 by default, up to 5,000. Each request returns up to 1,000 keywords of one domain, so 1,000 keywords is one lookup and 5,000 is five. Ask for more and the run continues at 5,000, with one uncharged row saying so.

## `sortBy` (type: `string`):

The order each domain's keywords come back in, which also decides which keywords a limit keeps: "traffic" (most estimated traffic first, the default), "volume" (highest search volume first) or "position" (best position first).

## `maxPosition` (type: `integer`):

Only keywords the domain ranks for at this position or better, a whole number from 1 to 100, e.g. 10 keeps the first page of Google and 3 the top three. The filter runs at the source, so lower rankings are never delivered or charged.

## `minSearchVolume` (type: `integer`):

Only keywords searched at least this many times a month, a whole number, e.g. 100. The filter runs at the source, so keywords below it are never delivered or charged.

## Actor input object example

```json
{
  "domains": [
    "ahrefs.com"
  ],
  "country": "US",
  "maxResultsPerDomain": 100,
  "sortBy": "traffic"
}
```

# Actor output Schema

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

One row per keyword a domain ranks for in organic Google results (rowType = keyword): position, absolute position, the ranking URL, search volume, CPC in USD, competition, keyword difficulty (0-100), search intent and estimated monthly traffic. A figure the source does not publish is null, never invented. Every row carries `charged`: only rows with charged = true were billed (plus one keyword-lookup per request of up to 1,000 keywords for one domain that delivered keywords). previousPosition, positionChange and isNew compare with the last run your account made for the same domain and market, and are null on a first run. One uncharged overview row per domain (rowType = domain_summary) carries the total keywords it ranks for, its position buckets and its estimated traffic. Rows with a missReason are uncharged notes saying why something was not delivered; the last row (rowType = run_summary) is the uncharged run receipt.

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

Domains looked up, ranked keywords delivered, what was charged, and what stopped the run.

# 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 = {
    "domains": [
        "ahrefs.com"
    ]
};

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

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

```

## MCP server setup

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