# Keyword Gap — Keywords Your Competitors Rank For (`amassuo/semrush-keyword-gap`) Actor

Compare your domain against competitors and get every keyword they rank for that you're missing, plus keywords where they outrank you. Sorted by search volume. Excel export included.

- **URL**: https://apify.com/amassuo/semrush-keyword-gap.md
- **Developed by:** [oussama dahbi](https://apify.com/amassuo) (community)
- **Categories:** SEO tools, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 keyword analyseds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Keyword Gap (Semrush)

Compare your domain against competitors and get **every keyword they rank for that you
don't** — plus keywords where they outrank you. Sorted by search volume, exported to Excel.

### How it works

```
your domain ─▶ domain_organic ─▶ your keyword map
competitors ─▶ domain_organic ─▶ compare each keyword
                                   ├─ not in your map        → gap_type "missing"
                                   ├─ their position better  → gap_type "weak"
                                   └─ you rank as well/better → skipped
                                 dedup across rivals (best position wins)
                                 sort by volume ─▶ dataset + Excel
```

### ⚠️ Billing model (important)

Semrush bills ~10 units for **every row fetched** — yours *and* each competitor's — not
just the gaps we surface. So this actor charges per **keyword analysed**
(`keyword_analyzed`), which keeps revenue aligned with cost regardless of how large the
gap turns out to be.

> Example: 1 domain + 2 competitors × 1,000 keywords = 3,000 analysed = 3,000 billable
> events, which might produce anywhere from 200 to 2,800 gap rows.

Charging per *gap row* instead would be dangerous: comparing two near-identical domains
returns almost no gaps while still costing full price in units.

### Safety rails

- **Charge-limit guard** — respects the user's max cost per run; stops before spending
  units it cannot bill.
- **`maxKeywordsPerDomain`** — bounds cost per domain.
- **Server-side filters** (`minVolume` / `maxPosition`) applied by Semrush *before*
  billing, so narrowing genuinely costs less.
- **Units pre-flight** — logs the estimate and your balance.
- **Fatal-error stop** — bad key/quota halts the run; delivered rows are kept.

### Setup

Set **`SEMRUSH_API_KEY`** as a secret environment variable on the actor.

### Run locally

```bash
cd semrush-keyword-gap
npm install
$env:SEMRUSH_API_KEY = "your-semrush-api-key"   # PowerShell
node src/main.js
```

Example input:

```json
{
  "yourDomain": "moz.com",
  "competitorDomains": ["ahrefs.com", "semrush.com"],
  "database": "us",
  "maxKeywordsPerDomain": 1000,
  "minVolume": 1000,
  "gapTypes": ["missing", "weak"]
}
```

### Input

| Field | Meaning |
|---|---|
| `yourDomain` | the domain you want gaps for (**required**) |
| `competitorDomains` / `competitorsText` | who to compare against |
| `database` | country market (`us`, `uk`, `de`…) |
| `maxKeywordsPerDomain` | rows pulled per domain — drives depth and cost |
| `minVolume` / `maxPosition` | server-side filters (also reduce cost) |
| `gapTypes` | `missing`, `weak`, or both |
| `excludeKeywords` | drop keywords containing these terms (applied server-side → also cheaper) |
| `xlsxOutput` | save formatted Excel (default on) |
| `semrushApiKey` | optional key override |

### Output

```json
{
  "keyword": "keyword research",
  "gap_type": "weak",
  "volume": 1830000,
  "cpc": 4.86,
  "your_position": 31,
  "best_competitor": "semrush.com",
  "their_position": 1,
  "their_url": "https://www.semrush.com/...",
  "competitors_ranking": ["semrush.com", "ahrefs.com"],
  "database": "us"
}
```

`gap_type: "missing"` rows have `your_position: null`.

### Publish + pay-per-event

```bash
apify login
apify push
```

**Publication ▸ Monetization ▸ Pay per event** — delete the auto-added
`apify-default-dataset-item` "Result" event (the code charges explicitly), then add:

| Event name | Title | Suggested price |
|---|---|---|
| `keyword_analyzed` | Keyword analysed | **$0.002** ($2 / 1,000) |

### Filtering out noise

Large domains rank for plenty of unrelated terms (Semrush ranks for random brand names
because of their traffic-checker pages). Use **`excludeKeywords`** to drop them:

```json
"excludeKeywords": ["xnxx", "simpcity", "casino", "porn"]
```

Up to 10 terms are pushed into Semrush's `display_filter` as negative
`-|Ph|Co|<term>` clauses, so excluded rows are **never fetched and never billed**;
anything beyond that is filtered client-side as a safety net.

### Roadmap

1. **Intent tagging** — classify each gap keyword with a cheap LLM pass.
2. **Cluster gaps by ranking URL** — see which of *their* pages to compete with.
3. **Scheduled runs** — track gap closure over time.

# Actor input Schema

## `yourDomain` (type: `string`):

The domain you want to find gaps for, e.g. moz.com

## `competitorDomains` (type: `array`):

Competitors to compare against.

## `competitorsText` (type: `string`):

Alternative: paste competitor domains separated by new lines or commas.

## `database` (type: `string`):

Semrush country database: us, uk, de, fr, es, it, ca, au, in…

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

Keywords pulled per domain (yours and each competitor), highest traffic first. Drives both depth and cost.

## `minVolume` (type: `integer`):

Ignore keywords below this monthly volume. Applied by Semrush before billing, so it also reduces cost.

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

Only consider keywords ranking at or above this position (e.g. 20).

## `gapTypes` (type: `array`):

missing = they rank, you don't at all. weak = you both rank but they beat you.

## `xlsxOutput` (type: `boolean`):

Also save a formatted Excel file to the run's key-value store.

## `semrushApiKey` (type: `string`):

Optional. Normally the operator sets SEMRUSH\_API\_KEY as an actor environment variable.

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

Drop keywords containing any of these terms (case-insensitive). Applied by Semrush before billing where possible, so it also reduces cost. Useful for stripping irrelevant/brand/adult terms big domains rank for.

## Actor input object example

```json
{
  "yourDomain": "moz.com",
  "competitorDomains": [
    "ahrefs.com",
    "semrush.com"
  ],
  "database": "us",
  "maxKeywordsPerDomain": 1000,
  "gapTypes": [
    "missing",
    "weak"
  ],
  "xlsxOutput": true
}
```

# Actor output Schema

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

One row per gap keyword: gap\_type (missing or weak), search volume, CPC, your position, the best competitor, their position and their ranking URL.

## `excel` (type: `string`):

Formatted spreadsheet of all keyword gaps, sorted by search volume.

# 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 = {
    "yourDomain": "moz.com",
    "competitorDomains": [
        "ahrefs.com",
        "semrush.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("amassuo/semrush-keyword-gap").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 = {
    "yourDomain": "moz.com",
    "competitorDomains": [
        "ahrefs.com",
        "semrush.com",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("amassuo/semrush-keyword-gap").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 '{
  "yourDomain": "moz.com",
  "competitorDomains": [
    "ahrefs.com",
    "semrush.com"
  ]
}' |
apify call amassuo/semrush-keyword-gap --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,amassuo/semrush-keyword-gap"
        }
    }
}

```

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/NVUoA0jvyWUos7NBd/builds/DVgoPkRslspBMQOk2/openapi.json
