# Competitor Keyword Research: Ranked Keywords API (`meridianlabs/competitor-keywords`) Actor

Competitor keyword research API: every keyword a website ranks for in Google, with position, ranking URL, search volume, CPC, intent and estimated traffic, plus organic competitors and a plain-English summary per domain. $0.03 per domain + $2 per 1,000 keywords.

- **URL**: https://apify.com/meridianlabs/competitor-keywords.md
- **Developed by:** [Meridian Labs](https://apify.com/meridianlabs) (community)
- **Categories:** SEO tools, Marketing, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $30.00 / 1,000 domain summaries

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

**Competitor Keyword Research** shows **every keyword a website ranks for in Google**: position, the **ranking URL**, **search volume**, **CPC**, competition, keyword difficulty, **search intent**, **estimated traffic** and the SERP features on the page. Add **organic competitors** (the sites ranking for the most of the same keywords) and you get a **plain-English summary per domain**: total ranked keywords, page-one keywords, estimated monthly traffic and what it's worth in ads, top pages and closest competitors. Analyse **up to 100 domains per run** and **up to 20,000 keywords per domain**. The input form is pre-filled, so the easiest way to try it is to click **Start**: 100 keywords for one domain cost $0.23.

It's built for **SEO specialists and content marketers** reverse-engineering a competitor, **agencies** preparing audits and pitches, **e-commerce teams** checking which product pages win search traffic, and **AI agents** that need ranking data through an API, without a monthly SEO-suite subscription.

### What data do you get for each domain?

**One row per ranked keyword** (`rowType: "keyword"`), most estimated traffic first:

- 🔑 **Keyword** and its **position** among Google's organic results (`position`; `absolutePosition` counts ads, maps and boxes too).
- 🔗 **Ranking URL** (`url`) and the page title shown in the results.
- 📈 **Search volume** (`searchVolume`, monthly, Google Ads Keyword Planner figure), **CPC** (USD), **competition** (0–1 and LOW/MEDIUM/HIGH).
- 🎯 **Keyword difficulty** (0–100) and **search intent** (informational, navigational, commercial, transactional).
- 🚦 **Estimated traffic** (`estimatedTraffic`): monthly visits this keyword brings, from its volume and the click-through rate at that position, plus what those visits would cost in ads (`trafficValueUsd`).
- 🧩 **SERP features** on the page (people also ask, local pack, images, video, AI overview and more), featured-snippet flag, and **movement** since the previous check (new, up, down).

**One row per organic competitor** (`rowType: "competitor"`, if you switch it on): the competitor's domain, **shared keywords** (and what share of yours that is), its average position on them, its traffic from them, and its **own total keywords and estimated traffic**.

**One summary row per domain** (`rowType: "summary"`, saved last):

- 📝 **A plain-English summary**, for example: *"heathceramics.com ranks for 9,813 keywords in the United States (English), 1,735 of them on page one (312 at #1). Estimated organic traffic: about 24,035 visits a month, worth about $23,067 a month in ads. Top page among the 100 keywords returned: https://www.heathceramics.com/ (~10,363 visits a month from 18 of them). Biggest keyword: "ceramics" (#2, 110,000 searches a month). Closest organic competitors: homedepot.com, target.com, crateandbarrel.com."*
- 📊 **Totals for the whole domain**: ranked keywords, #1 / top-3 / page-one keywords, estimated monthly traffic and its ad value, keywords new, up and down since the last check.
- 🏆 **Top pages** by estimated traffic among the keywords returned.

### Why use this competitor keyword tool?

- 🧾 **The whole list, not a teaser.** Ask for 5,000 keywords and you get 5,000 when the domain ranks for that many (up to 20,000 per domain).
- 🗣️ **A summary you can paste into a report.** Every domain gets totals and a readable paragraph, so you don't need a spreadsheet to say how a competitor is doing.
- 🎯 **Filters that save money.** Minimum search volume, a position range (e.g. 4–20 for "almost page one" keywords) and excluded terms are applied by the data provider before you're charged.
- 💸 **Fair billing.** You pay per domain analysed and per row returned. Failed lookups, invalid entries and domains that don't exist are free, and the run stops exactly at your maximum spending limit.
- 🤖 **AI-agent ready.** Documented output schema, three table views and stable field names. It works from the Apify API, integrations (Make, Zapier, n8n, Google Sheets) and MCP clients.

### How do I see which keywords a competitor ranks for?

1. Enter one or more **domains** (`competitor.com`), one per line. `https://www.competitor.com/page` works too: it's reduced to the domain.
2. Pick the **country** (default United States) and, if it has several, the **language**.
3. Set **Max keywords per domain** (default 1,000) and any filters: **Minimum monthly searches**, **Best / Worst position to include**, **Exclude keywords containing**.
4. Switch on **Find organic competitors** if you want them.
5. Click **Start**. Open the **Ranked keywords**, **Organic competitors** or **Summary per domain** view on the Output tab, or export everything as JSON, CSV or Excel.

#### How do I find a competitor's best pages?

Run the domain with **Max keywords per domain** at 1,000 or more. The summary row lists the **top pages** by estimated traffic; for the full picture, open the **Ranked keywords** view and sort or group by `url`.

#### How do I find keywords where a site is almost on page one?

Set **Best position to include** to 4 and **Worst position to include** to 20, and **Minimum monthly searches** to, say, 100. You get the keywords ranking #4–20 with real demand: the usual quick wins.

#### How do I find my organic competitors?

Enter your own domain, switch on **Find organic competitors** and set **Max keywords per domain** to 0 if you only want the competitor list and the summary. Each competitor row says how many keywords you share and how much traffic it gets overall. Giant general sites (Google, YouTube, Facebook, Wikipedia and similar) are left out.

### How much does it cost?

**$0.03 per domain analysed, plus $0.002 per ranked-keyword row ($2 per 1,000 keywords) and $0.002 per competitor row. Failed lookups are free.**

| Event | Price | Charged when |
|---|---|---|
| Domain summary | $0.03 per domain | The lookup for a domain answered, including a clean "ranks for no keywords" for a site that exists: the lookup ran and answered. |
| Ranked keyword | $0.002 per row | Each keyword row saved. |
| Organic competitor | $0.002 per row | Each competitor row saved (only if you switch competitors on). |

- 🆓 **Free:** errors, invalid entries, **domains that don't exist** (no DNS record: they aren't even looked up), and domains or rows skipped because the run reached your **maximum spending limit** or its **time limit**.
- **Examples:** 1 domain with its top 100 keywords = $0.23. 1 domain with 1,000 keywords = $2.03. 5 competitors × 1,000 keywords = $10.15. 1 domain, summary and 10 competitors only = $0.05.
- **Free to try:** Apify's free plan includes $5 of platform usage every month: about **2,400 keyword rows**, or 160 domain summaries. Platform usage is included in the price.
- Set a **maximum cost per run** in the run options: the Actor only fetches rows it can charge for and stops exactly there. Domains it didn't reach are listed as `skipped` and not charged.

### Output example

Data source: DataForSEO Labs (Google rankings from DataForSEO's own search-results database; search volume, CPC and competition are Google Ads Keyword Planner figures), stated on every row as `dataSource`.

A keyword row (real output, October 2026):

```json
{
  "rowType": "keyword",
  "domain": "heathceramics.com",
  "keyword": "ceramics",
  "position": 2,
  "absolutePosition": 6,
  "url": "https://www.heathceramics.com/",
  "searchVolume": 110000,
  "cpc": 1.29,
  "competition": 0.07,
  "competitionLevel": "LOW",
  "keywordDifficulty": 52,
  "intent": "informational",
  "estimatedTraffic": 5657.8,
  "trafficValueUsd": 7298.53,
  "serpFeatures": ["local_pack", "people_also_ask", "related_searches", "images"],
  "isFeaturedSnippet": false,
  "rankChange": "up",
  "country": "US",
  "language": "en"
}
```

A competitor row:

```json
{
  "rowType": "competitor",
  "domain": "heathceramics.com",
  "competitorDomain": "crateandbarrel.com",
  "sharedKeywords": 2501,
  "sharedKeywordsPct": 25.5,
  "avgPositionOnShared": 21.1,
  "sharedKeywordsTraffic": 7036.6,
  "competitorKeywords": 607500,
  "competitorTop10Keywords": 107617,
  "competitorTraffic": 1724760.7
}
```

A summary row (trimmed):

```json
{
  "rowType": "summary",
  "domain": "heathceramics.com",
  "status": "ok",
  "rankedKeywords": 9813,
  "firstPositionKeywords": 312,
  "top10Keywords": 1735,
  "estimatedMonthlyTraffic": 24035,
  "trafficValueUsd": 23067,
  "summary": "heathceramics.com ranks for 9,813 keywords in the United States (English), 1,735 of them on page one (312 at #1). ...",
  "topPage": "https://www.heathceramics.com/",
  "keywordsReturned": 100,
  "keywordsLimitedBy": "maxKeywordsPerDomain",
  "competitorsReturned": 3,
  "charged": true
}
```

Summary `status` is one of:

- `ok`: the domain ranks for keywords.
- `no_rankings`: the site exists but ranks for no keywords in that country (or none matching your filters). Charged: the lookup ran and answered.
- `error`: the lookup failed, the entry isn't a valid domain, or the domain doesn't exist (`DOMAIN_NOT_FOUND`). Not charged.
- `skipped`: not analysed because the run hit its spending, time or data-cost limit. Not charged.

`keywordsReturned` says how many keyword rows you got and `keywordsLimitedBy` why you got fewer than the domain ranks for (`maxKeywordsPerDomain`, `SPENDING_LIMIT`, `RUN_TIME_LIMIT`). For each domain, its keyword and competitor rows come first and the summary row last.

### Input

| Field | What it does |
|---|---|
| `domains` | Domains to analyse, one per line (up to 100). `https://www.example.com/page` counts as `example.com`; other subdomains (`blog.example.com`) are analysed on their own. Duplicates are removed. |
| `country`, `language` | The Google market (94 countries; default US). Language is optional: empty = the country's main language. |
| `maxKeywordsPerDomain` | Most keyword rows per domain, most estimated traffic first (0–20,000; default 1,000). 0 = summary and competitors only. |
| `minSearchVolume` | Only keywords searched at least this often a month. |
| `minPosition`, `maxPosition` | Position range to include (1–100), e.g. 1–10 for page one. |
| `excludeKeywords` | Up to 5 terms; keywords containing any of them are dropped (e.g. the domain's own brand). |
| `includeCompetitors`, `maxCompetitors` | Also return organic competitors, most shared keywords first (1–100; default 10). |
| `failOnErrors` | Mark the run failed if any lookup errors (for schedules and monitoring). Failed lookups are still free. |

Example:

```json
{
  "domains": ["heathceramics.com", "eastfork.com"],
  "country": "US",
  "maxKeywordsPerDomain": 1000,
  "minSearchVolume": 50,
  "maxPosition": 20,
  "includeCompetitors": true,
  "maxCompetitors": 10
}
```

#### Switching from another competitor-keywords Actor?

These input names work too, so an existing input usually runs unchanged: `domainsText` (a pasted list), `database` (country market such as `us` or `uk`), `minVolume`, `discoverCompetitors` and `competitorsPerDomain`. `maxKeywordsPerDomain`, `maxPosition` and `excludeKeywords` mean the same here.

### How to read the numbers

- **Estimated traffic is a model, not analytics.** It's search volume × the typical click-through rate at that position, summed over keywords. Use it to compare sites and pages, not as a visit count.
- **Totals cover the whole domain; top pages cover the rows returned.** With filters on, the totals count only matching keywords (the summary says so).
- **Positions and volumes differ between tools.** Every ranking database checks results pages on its own schedule (`serpCheckedAt` on each row); compare within one source.

### How do I track a competitor's rankings every month?

1. Fill in the input (the competitor's domain, a keyword limit) and click **Save as a new task**.
2. Open **Schedules** → **Create new**, pick the task and choose **monthly**.
3. Compare runs on `position` and `rankChange`, or send the dataset to a spreadsheet (next section).

### How do I send keyword data to Google Sheets, Make, Zapier, n8n or an AI agent?

- **Google Sheets (one formula):** save your input as a task and schedule it, then put this in cell A1 of a sheet and click **Allow access** on the yellow bar Sheets shows about external data:
  `=IMPORTDATA("https://api.apify.com/v2/acts/meridianlabs~competitor-keywords/runs/last/dataset/items?status=SUCCEEDED&format=csv&view=keywords&token=YOUR_TOKEN")`
  It loads your latest successful run, and picks up a newer one when Google refreshes imported data (about once an hour) or when you reopen the sheet. For the other tables, change `view=keywords` to `view=summary` or `view=competitors`. Anyone who can open the sheet can see the token, so create one with limited permissions in Console → Settings → API & Integrations. For a saved task, use `actor-tasks/<your-username>~<task-name>/runs/last/...` instead.
- **Make:** Apify "Run an Actor" (Run synchronously: Yes) → Apify "Get Dataset Items" → Google Sheets, add rows.
- **n8n:** Apify trigger "Actor Run Finished" → Apify "Get Items" (Datasets) with the run's `defaultDatasetId` → Google Sheets, append.
- **Zapier:** trigger "Finished Actor run" → "Fetch dataset items" → Google Sheets.
- **Webhooks:** Integrations → **HTTP webhook** on "Run succeeded" sends the run details to your URL.
- **AI agents (MCP):** add `https://mcp.apify.com?tools=meridianlabs/competitor-keywords` to your MCP client (Claude, Cursor, VS Code and others).

### Limitations

- **Google only**, top 100 organic positions, one country and language per run.
- **Up to 20,000 keywords per domain** and 100 domains per run.
- **Pages are reduced to their domain.** For a single page's keywords, filter the keyword rows by `url`.

### FAQ

#### Where does the data come from?

From **DataForSEO Labs**: rankings come from DataForSEO's own regularly refreshed database of Google results pages, and search volume, CPC and competition are Google Ads Keyword Planner figures. This Actor queries it live on every run; it isn't a scraper of any SEO tool's website, so it doesn't break when a website changes. Not affiliated with Google.

#### Can I use it with the API, AI agents or MCP?

Yes. Run it through the Apify API or client libraries, schedule it, connect it to Make, Zapier, n8n or Google Sheets, or call it from MCP clients.

Python (`pip install apify-client`):

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("meridianlabs/competitor-keywords").call(run_input={
    "domains": ["heathceramics.com"], "maxKeywordsPerDomain": 500, "maxPosition": 10,
})
for row in client.dataset(run.default_dataset_id).iterate_items():
    if row["rowType"] == "keyword":
        print(row["keyword"], row["position"], row["searchVolume"], row["url"])
```

#### Does it collect personal data?

No. Rows contain keywords, public page URLs and titles, and aggregate search statistics. The Actor doesn't store anything in your account beyond the run's own dataset.

#### What happens if a run times out or restarts?

If a run approaches its timeout, the Actor stops starting new lookups: domains it didn't reach are listed as `skipped` (not charged), and a domain cut short keeps the rows already delivered, with `keywordsLimitedBy: "RUN_TIME_LIMIT"`. If the platform restarts a run, it picks up where it left off without charging twice.

### More from Meridian Labs

- **[Backlink Checker](https://apify.com/meridianlabs/backlink-checker)**: domain rank, backlinks, referring domains and spam score for up to 1,000 domains, plus full backlink and referring-domain lists. $0.03 per domain + $4 per 1,000 rows.
- **[Keyword Search Volume & CPC API + Google Trends](https://apify.com/meridianlabs/keyword-search-volume)**: monthly search volume, CPC, competition, difficulty and intent for keyword lists, keyword ideas from a seed, plus a Google Trends summary per keyword. From $3 per 1,000 keywords.
- **[Google Trends Scraper & API](https://apify.com/meridianlabs/google-trends-scraper)**: interest over time, by region and rising related searches for hundreds of keywords, as a maintained pytrends alternative. $5 per 1,000 keyword lookups.
- **[Greenhouse, Lever & Ashby Jobs Scraper](https://apify.com/meridianlabs/greenhouse-lever-ashby-jobs-scraper)**: open jobs from companies on six hiring systems, with salary normalised to min, max, currency and period. $2 per 1,000 jobs.
- **[Startup Funding Rounds & Founders API](https://apify.com/meridianlabs/sec-form-d-funding-rounds)**: US startups that just raised, from SEC Form D filings: amount raised, investor count, founders and executives, and a one-line summary. $10 per 1,000 filings.
- **[EU & UK Tenders Scraper](https://apify.com/meridianlabs/eu-uk-tenders-scraper)**: public tenders and contract awards from the EU's TED and the UK's Find a Tender, with buyer, value, deadline and a one-line summary. $3 per 1,000 notices.

### Support

Something looks wrong, or you need a field we don't return? Open an issue on the **Issues** tab with the input you used. We read every one.

If this saved you time, a rating helps others find it; if something's off, open an issue and we'll fix it fast.

# Actor input Schema

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

Domains to analyse, one per line, up to 100 per run: your competitors, prospects or your own site. Paste with or without https:// and www.; a page URL counts as its domain. Duplicates are removed.

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

The Google market whose rankings you want (search volumes and traffic are for this country too).

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

Language code or name, e.g. en, fr or French. Leave empty for the country's main language. Only matters in multi-language countries (e.g. Canada: en/fr; Switzerland: de/fr/it).

## `maxKeywordsPerDomain` (type: `integer`):

How many ranked keywords to return per domain, most estimated traffic first. Each keyword row costs $0.002, so this is your cost cap per domain. 0 = summary (and competitors) only.

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

Optional. Only keywords searched at least this often a month. Filtered by the data provider before you are charged.

## `minPosition` (type: `integer`):

Optional. Leave empty to start at #1. Example: 4 with a maximum of 20 finds the 'almost page one' keywords (positions 4-20).

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

Optional. 10 = page-one rankings only, 3 = top three only.

## `excludeKeywords` (type: `array`):

Optional, up to 5 terms. Drops keywords containing any of them (e.g. the domain's own brand name), before you are charged.

## `includeCompetitors` (type: `boolean`):

Also return each domain's organic competitors: the sites ranking for the most of the same keywords, with shared keywords and their own traffic. $0.002 per competitor. Giant general sites (Google, YouTube, Facebook, Wikipedia and similar) are left out.

## `maxCompetitors` (type: `integer`):

How many competitors to return per domain, most shared keywords first.

## `failOnErrors` (type: `boolean`):

Useful for schedules and monitoring: the run is marked failed if any domain errors. Failed lookups are still not charged.

## Actor input object example

```json
{
  "domains": [
    "heathceramics.com"
  ],
  "country": "US",
  "maxKeywordsPerDomain": 100,
  "includeCompetitors": false,
  "maxCompetitors": 10,
  "failOnErrors": false
}
```

# Actor output Schema

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

One row per keyword each domain ranks for (dataset view 'keywords').

## `competitors` (type: `string`):

One row per organic competitor (dataset view 'competitors').

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

One row per domain: plain-English summary, totals, estimated traffic, top page (dataset view 'summary').

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

Every field of every row.

# 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": [
        "heathceramics.com"
    ],
    "country": "US",
    "maxKeywordsPerDomain": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("meridianlabs/competitor-keywords").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": ["heathceramics.com"],
    "country": "US",
    "maxKeywordsPerDomain": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("meridianlabs/competitor-keywords").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": [
    "heathceramics.com"
  ],
  "country": "US",
  "maxKeywordsPerDomain": 100
}' |
apify call meridianlabs/competitor-keywords --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,meridianlabs/competitor-keywords"
        }
    }
}
```

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/KrVFH3IVVzzd2SbRm/builds/kC3lkc1bZzJ7j1bjh/openapi.json
