# Keyword Gap & Opportunity Report - Competitor Keyword Gap (`wrenfield-data/keyword-gap-report`) Actor

Keywords up to 3 competitors rank for on Google and your client's domain doesn't: volume, difficulty, intent, competing URLs and why each was picked. Top 20 re-checked on the live Google top 10. CSV-ready dataset + HTML report. One flat price per report.

- **URL**: https://apify.com/wrenfield-data/keyword-gap-report.md
- **Developed by:** [Wrenfield Data](https://apify.com/wrenfield-data) (community)
- **Categories:** SEO tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$9,000.00 / 1,000 keyword gap reports

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

## Keyword Gap & Opportunity Report: keywords your competitors rank for and you don't

> **$9 per completed report.** The Store price box may show this as "$9,000 / 1,000 reports": it is $9 per report. Up to 3 competitors, up to 300 gap keywords, the top 20 re-checked on live Google. Not charged if a competitor fails or nothing matches your filters.

Enter **one domain** (yours or a client's) and **up to 3 competitors**. You get the Google keywords those competitors rank for and your domain doesn't, deduplicated and filtered, each with **search volume, keyword difficulty, intent, the competing URLs and a plain-language reason it was picked**. The best 20 are then **re-checked on the live Google top 10**, so you know which gaps are still open today, not just in a months-old database snapshot.

- 🎯 **Real gaps, not noise**: competitor brand searches, login/navigational queries and hosted subdomains (for example customer help centers on `*.zendesk.com`) are removed by default. Add your own exclusions and topic terms.
- 🧭 **Topic fit**: every keyword is scored against the words on your own homepage, so "customer service training" outranks "margin calculator" for a help desk company.
- ✅ **Live check of the top 20**: open gap, client already ranks, or the database ranking is out of date. Includes whether an AI Overview shows and whether it cites you.
- 🕒 **Honest timestamps**: when the provider *observed* each ranking vs when this run *retrieved* it, and when Google was checked live. Zero search volume and *unavailable* are never mixed up.
- 📄 **CSV-ready dataset + compact HTML report** (`REPORT.html`, printable, no third-party branding; add your agency name).

### What a report looks like

Real rows from a run on Oct 22, 2026: `helpscout.com` against `zendesk.com`, `freshworks.com` and `intercom.com` (United States, English). It found 235 gap keywords after filters, took 29 seconds, and re-checked the top 20 on live Google.

| Keyword | Volume / mo | KD | Competitor in database | Live Google, Oct 22 |
| --- | ---: | ---: | --- | --- |
| ticketing system | 1,900 | 15 | zendesk.com #1 | open gap: zendesk.com #8 |
| customer service training | 8,100 | 18 | zendesk.com #4 | open gap: zendesk.com #2 |
| ai powered customer platform | 1,300 | 33 | freshworks.com #1 | open gap: intercom.com #4 |
| customer service desk | 480 | 8 | freshworks.com #1 | open gap: freshworks.com #1 |
| email thread | 6,600 | 3 | intercom.com #4 | competitors not in top 10 now |

The last row is why the live check exists: the database said Intercom ranked #4, but on Oct 22 no competitor was in the Google top 10. Of the 20 keywords checked live, 8 were open gaps and 12 had moved.

### How to use it

1. Enter the **domain** you want new keywords for (for example `helpscout.com`).
2. Add **1 to 3 competitors** that actually outrank you in Google (search competitors, not only business rivals).
3. Optional: **topic terms** (keep only keywords about, for example, `help desk`, `support`), **exclusions** (`jobs`, product names), minimum volume, maximum difficulty.
4. Click **Start**. A report takes about 30 seconds. Open **REPORT.html** in the Output tab, or export the dataset as CSV or Excel.

### Input example

```json
{
  "clientDomain": "helpscout.com",
  "competitors": ["zendesk.com", "freshworks.com", "intercom.com"],
  "location": "United States",
  "language": "English",
  "excludeTerms": ["freshdesk", "freshservice", "fin ai"],
  "minSearchVolume": 30,
  "maxCompetitorPosition": 20,
  "liveSerpChecks": 20,
  "preparedBy": "Northwind Digital"
}
```

### Output example

One row per gap keyword (real row from a run on Oct 22, 2026; trimmed):

```json
{
  "rank": 5,
  "keyword": "customer service training",
  "opportunity_score": 74.9,
  "search_volume": 8100,
  "search_volume_status": "measured",
  "search_volume_month": "2026-08",
  "cpc_usd": 14.64,
  "keyword_difficulty": 18,
  "search_intent": "informational",
  "topic_fit": 1,
  "competitors_ranking": 1,
  "competitor_positions": "zendesk.com #4",
  "best_competitor_url": "https://www.zendesk.com/blog/customer-service/support/what-is-customer-service/customer-service-training/",
  "selection_reasons": "1 of 3 competitors ranks for it: zendesk.com #4; 8,100 Google searches a month in United States; keyword difficulty 18 (easy); informational intent; matches the client's homepage wording (customer, train); ...; live Google check 2026-10-22: helpscout.com not in the top 10; zendesk.com #2; AI Overview shown",
  "live_verdict": "open gap",
  "competitor_positions_live": "zendesk.com #2",
  "top10_domains_live": "www.dalecarnegie.com, www.zendesk.com, www.amanet.org, www.reddit.com, ...",
  "ai_overview_live": true,
  "ai_overview_cites_client": false,
  "labs_serp_observed_at": "2026-07-30T12:58:52Z",
  "labs_retrieved_at": "2026-10-22T23:45:12Z",
  "serp_checked_at": "2026-10-22T23:45:22Z"
}
```

That run found 235 gap keywords after filters. Of the 20 re-checked live, 8 were open gaps with a competitor in today's top 10, and 12 had a competitor that was no longer in the top 10 (the database snapshot was from July to October). That is why the live check exists.

**Key-value store:** `REPORT.html` (the report), `REPORT` (JSON summary: filters, per-competitor counts, top picks, observation date range, the homepage words used for topic fit).

#### Columns

| Column | Meaning |
| --- | --- |
| `opportunity_score` | 0-100: 25% demand, 20% ease (100 - KD), 20% topic fit, 15% how many competitors rank, 10% intent, 10% best competitor position |
| `search_volume`, `search_volume_status` | Monthly Google searches (Google Ads data); status `measured`, `zero` or `unavailable` (no value, not the same as zero) |
| `keyword_difficulty`, `search_intent` | KD 0-100 and main intent, each with a `_status` column |
| `topic_fit` | Share of the keyword's words found on your homepage (null if the site could not be read) |
| `competitor_positions`, `competing_urls` | Which competitors rank, where, and with which page (database) |
| `selection_reasons` | Why the keyword is in the report, in plain language |
| `live_verdict` | `open gap`, `client already ranks`, `competitors not in top 10 now`, `check failed` or `not checked` |
| `labs_serp_observed_at` / `labs_retrieved_at` | When the provider observed the rankings / when this run queried them |
| `serp_checked_at` | When the live Google top 10 was observed |

### Pricing

**$9 per completed report**, all-inclusive: up to 3 competitors, up to 100 candidates each (up to 300 keywords after merging), filtering, scoring, 20 live Google checks and the HTML report. No per-keyword or per-request fees. Apify platform usage for a run is a fraction of a cent.

**Not charged:** a run where any competitor's data request fails (the partial report is still saved), a run where nothing matches your filters, or a run whose maximum cost is below $9 (it stops before collecting anything).

**Free Apify plan:** up to 3 free previews a week (1 competitor, 25 candidates, 5 live checks), not charged.

### Method and limits

- Candidates come from a licensed SEO database (DataForSEO Labs, Google results): keywords where a competitor ranks and your domain has no top-100 ranking, ordered by the competitor's estimated traffic. Rankings in the database are refreshed on the provider's schedule, typically every few weeks to months, which is why every row carries the observation date.
- Live checks are real Google desktop results (top 10) for the chosen country and language at run time.
- Traffic estimates are modeled by the provider, not analytics data.
- Topic fit reads your homepage only (title, headings, links, descriptions). If the site blocks automated requests, fit is marked unavailable and scored neutral.

### Keyword Gap Report: REST, Python, JavaScript, n8n and MCP

Actor ID: `wrenfield-data/keyword-gap-report`. Authenticate with your [Apify API token](https://console.apify.com/settings/integrations) in an `Authorization: Bearer` header. One run is one report, so set `maxTotalChargeUsd` to at least `10` (the report is $9). A lower cap makes the run stop before collecting anything, free of charge.

```bash
curl -X POST "https://api.apify.com/v2/acts/wrenfield-data~keyword-gap-report/run-sync-get-dataset-items?maxTotalChargeUsd=10" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"clientDomain": "yourdomain.com", "competitors": ["competitor1.com", "competitor2.com"]}'
```

The synchronous endpoint waits up to 300 seconds, and a report usually takes about 30. It returns the dataset rows as a JSON array.

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("wrenfield-data/keyword-gap-report").call(
    run_input={"clientDomain": "yourdomain.com", "competitors": ["competitor1.com", "competitor2.com"]},
    max_total_charge_usd=10,
)
for row in client.dataset(run.default_dataset_id).iterate_items():  # apify-client 1.x/2.x: run["defaultDatasetId"]
    if row.get("type") == "notice":
        print("NOTE:", row["note"]); continue
    print(row["rank"], row["keyword"], row["search_volume"], row["keyword_difficulty"], row["live_verdict"])
```

**JavaScript** (`npm install apify-client`):

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('wrenfield-data/keyword-gap-report').call(
    { clientDomain: 'yourdomain.com', competitors: ['competitor1.com', 'competitor2.com'] },
    { maxTotalChargeUsd: 10 },
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const gaps = items.filter((r) => r.type !== 'notice');
console.log(gaps.slice(0, 5).map((r) => `${r.keyword} ${r.search_volume} KD${r.keyword_difficulty} ${r.live_verdict}`));
```

**Async with a webhook:** `POST /v2/acts/wrenfield-data~keyword-gap-report/runs?maxTotalChargeUsd=10` returns immediately. Add a webhook for `ACTOR.RUN.SUCCEEDED`, then fetch `GET /v2/datasets/{defaultDatasetId}/items?format=csv`. `REPORT.html` is in the run's key-value store at `GET /v2/key-value-stores/{defaultKeyValueStoreId}/records/REPORT.html`.

**n8n / Make / Zapier:** use the Apify node or app and choose *Run an Actor and Get Dataset* (n8n) or *Run an Actor* followed by *Get Dataset Items*. Paste the JSON input above. Schedule it monthly per client to see new gaps, and send rows to Google Sheets.

**MCP (Claude, Cursor, VS Code or any MCP client):** `https://mcp.apify.com?tools=wrenfield-data/keyword-gap-report`

**Notice rows:** if a run stops early (maximum cost below $9, or the weekly free-preview limit was reached), the dataset holds a single row `{"type": "notice", "note": "..."}` that explains why. Filter on `type` in pipelines.

**Stable fields:** we add fields but don't rename or remove the ones documented above. Optional fields can be null or absent.

### For agencies: run your whole client book

Reporting to clients every week? [Agency AI Visibility Reports](https://apify.com/wrenfield-data/agency-ai-visibility-reports) turns a client CSV into a white-label packet per client: AI visibility on ChatGPT, Perplexity, Gemini and Google AI Mode, competitor share of voice, Google rankings, quoted evidence and an action queue. **$12 per client packet** (the Store lists it as "$12,000 / 1,000 packets"; that is $12 each, less for 20+ clients).

### FAQ

**How is this different from a competitor keywords export?** An export gives you everything a competitor ranks for. This keeps only what you don't rank for, removes brand and login searches, merges duplicates across competitors, scores fit with your site, explains every pick, and verifies the top 20 against today's Google.

**Can I use page URLs?** Domains only (paths are removed). Rankings count the whole domain.

**Which countries?** Every country the keyword database covers (90+). Use a country name, 2-letter code or location code.

**What if my site is new and ranks for nothing?** It works: every competitor keyword is then a gap. Use topic terms to stay on topic.

### Related tools

- [Competitor Keywords](https://apify.com/wrenfield-data/competitor-keywords): every keyword a domain ranks for
- [Keyword Search Volume](https://apify.com/wrenfield-data/keyword-search-volume): Google Ads volumes for any list
- [Keyword Difficulty Checker](https://apify.com/wrenfield-data/keyword-difficulty-checker): KD and intent in bulk
- [SEO Toolkit All-in-One](https://apify.com/wrenfield-data/seo-toolkit): domain overview, backlinks and SERPs

### Feedback and support

Need more competitors, another market or a different scoring? Open an issue on the **Issues** tab with the run ID. We reply there, usually within a day.

### Acceptable use

Independent tool, not affiliated with Google, DataForSEO or the companies in the examples. Brands appear only because their public Google rankings were analyzed.

# Actor input Schema

## `clientDomain` (type: `string`):

The site you want new keywords for, e.g. <code>helpscout.com</code>. <code>https://</code>, <code>www.</code> and paths are removed.

## `competitors` (type: `array`):

1 to 3 competitors that rank where you want to rank, e.g. <code>zendesk.com</code>. Pick real search competitors (the sites that outrank you), not only business rivals.

## `location` (type: `string`):

Country of the Google results: name (<code>United States</code>), 2-letter code (<code>GB</code>) or location code (<code>2840</code>).

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

Language name or code (<code>English</code>, <code>de</code>). Empty = main language of the country.

## `excludeTerms` (type: `array`):

Drop keywords that contain any of these words or phrases, e.g. <code>jobs</code>, <code>free download</code>, your own brand. Case-insensitive.

## `excludeCompetitorBrands` (type: `boolean`):

Drop keywords containing a competitor's name taken from its domain (for example <i>zendesk</i> from zendesk.com). Usually what you want: you can't win "zendesk pricing".

## `topicTerms` (type: `array`):

Keep only keywords containing at least one of these words, e.g. <code>help desk</code>, <code>support</code>, <code>ticket</code>. Useful when competitors are broad sites.

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

Skip keywords with fewer Google searches a month. 0 also keeps keywords whose volume is unavailable.

## `maxKeywordDifficulty` (type: `integer`):

Skip keywords harder than this (0-100). 100 = no limit.

## `maxCompetitorPosition` (type: `integer`):

Only count a competitor ranking at this position or better. 20 = first two pages (recommended); 10 = page one only.

## `includeNavigational` (type: `boolean`):

Keep brand and login searches ("x login", "x support phone"). Off by default: they are rarely winnable.

## `includeCompetitorSubdomains` (type: `boolean`):

Also count rankings on competitor subdomains. Off by default, because hosted subdomains (for example customer help centers on <code>\*.zendesk.com</code>) are not the competitor's own content.

## `candidatesPerCompetitor` (type: `integer`):

How many gap keywords to pull per competitor (highest competitor traffic first), before merging duplicates. Max 100, so a report has up to 300.

## `liveSerpChecks` (type: `integer`):

How many of the best candidates to re-check on the live Google top 10 (confirms the gap is still open today). Max 20. 0 = database only.

## `preparedBy` (type: `string`):

Your name or agency, shown on the HTML report. No Wrenfield branding appears in the report.

## `maxSpendUsd` (type: `integer`):

Optional. The run stops before collecting anything, and charges nothing, if the report price is above this. Same effect as "Maximum cost per run" in Run options.

## Actor input object example

```json
{
  "clientDomain": "helpscout.com",
  "competitors": [
    "zendesk.com",
    "freshworks.com",
    "intercom.com"
  ],
  "location": "United States",
  "language": "English",
  "excludeCompetitorBrands": true,
  "minSearchVolume": 30,
  "maxKeywordDifficulty": 100,
  "maxCompetitorPosition": 20,
  "includeNavigational": false,
  "includeCompetitorSubdomains": false,
  "candidatesPerCompetitor": 100,
  "liveSerpChecks": 20
}
```

# Actor output Schema

## `report` (type: `string`):

No description

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

No description

## `csv` (type: `string`):

No description

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

No description

# 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 = {
    "clientDomain": "helpscout.com",
    "competitors": [
        "zendesk.com",
        "freshworks.com",
        "intercom.com"
    ],
    "location": "United States",
    "language": "English"
};

// Run the Actor and wait for it to finish
const run = await client.actor("wrenfield-data/keyword-gap-report").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 = {
    "clientDomain": "helpscout.com",
    "competitors": [
        "zendesk.com",
        "freshworks.com",
        "intercom.com",
    ],
    "location": "United States",
    "language": "English",
}

# Run the Actor and wait for it to finish
run = client.actor("wrenfield-data/keyword-gap-report").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 '{
  "clientDomain": "helpscout.com",
  "competitors": [
    "zendesk.com",
    "freshworks.com",
    "intercom.com"
  ],
  "location": "United States",
  "language": "English"
}' |
apify call wrenfield-data/keyword-gap-report --silent --output-dataset

```

## MCP server setup

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

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/IiCeo2FXhvrka64jp/builds/x7OJcuaomGoMofEIg/openapi.json
